1 前言
我們都知道,java提供了8種基本資料類型供我們使用,此外,java還提供了這些基本資料類型所對應的包裝類,而且通過自動裝箱和自動拆箱機制能夠輕松地完成基本資料類型和包裝類之間的轉換。
- 自動裝箱:自動将基本資料類型轉換為包裝類。
- 自動拆箱:自動将包裝類轉換為基本資料類型。
2 Integer
2.1 總體結構
首先,我們來看一下Integer類的總體結構,如下圖所示:
Integer類圖結構
- Integer繼承了Number類,并重寫了Number類intValue()、longValue()、floatValue()等方法來完成對一些基本資料類型的轉換
- Integer類實作了Comparable接口,這使得我們可以重寫compareTo方法來自定義Integer對象之間的比較操作
2.2 注釋
從Integer類的注釋中我們可以擷取到以下資訊:
- Integer類将原始類型int的值包裝在一個對象中,Integer類的對象使用一個int類型的字段value來表示對象的值
- 此外,Integer類還提供了一系列的方法來完成int到String,int到其他基本資料類型,String到int的轉換
和String類一樣,Integer類也是不可變類,Integer類使用final進行修飾,而且用于表示Integer對象值的字段value也使用了final進行修飾,Java中的所有包裝類都是不可變類。
2.3 自動裝箱、拆箱原理
通過以下一段簡單的代碼我們就能了解到自動裝箱、拆箱的過程:
public class IntegerDemo {
public static void main(String[] args) {
// 自動裝箱
Integer numberI = 66;
// 自動拆箱
int number = numberI;
}
}
那麼包裝類的自動裝箱、拆箱究竟是如何實作的呢?
我們可以通過javap反編譯或者通過idea中的插件jclasslib來了解這一過程,如下圖所示:
可以看到,Integer類是通過調用自身内部的valueOf()方法來實作自動裝箱的,而自動拆箱則是通過調用繼承自Number類的intValue()方法來實作的。
2.4 緩存
在《阿裡巴巴 Java 開發手冊》中有一段關于包裝對象之間值的比較問題的規約:就是所有整型包裝類對象之間值的比較,全部使用 equals 方法比較。而手冊中對這條規約的說明就是:對于 Integer var = ? 在 - 128 至 127 範圍内的指派,Integer 對象是在 IntegerCache.cache() 方法中産生的,會複用已有對象,這個區間内的 Integer 值可以直接使用 == 進行判斷,但是這個區間之外的所有資料,都會在堆上産生,并不會複用已有對象,是以推薦使用 equals 方法進行值的判斷。
讓我們來看一下這樣一段代碼:
Integer a = 100, b = 100, c = 150, d = 150;
System.out.println(a == b);
System.out.println(c == d);
有了上文的分析,我們很輕松地就能知道答案:true,false。這是因為對于 - 128 至 127 範圍内的值,Integer對象會使用緩存,是以b會複用a對象,而超出了緩存區間的 150 則不會使用緩存,是以會建立新的對象。
從上文對自動裝箱的分析我們可以得知,每次對Integer對象進行指派都會調用其valueOf()方法,接下來我們從源碼來分析Integer類的緩存機制:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以得知,Integer的緩存機制是通過它内部的一個靜态類IntegerCache來實作的,在IntegerCache.low和IntegerCache.high之間的值将直接使用IntegerCache的cache數組中緩存的值,否則會建立一個新的對象。同時,從源碼的注釋中我們還可以得知:如果不要求必須建立一個整型對象,緩存最常用的值(提前構造緩存範圍内的整型對象),會更省空間,速度也更快。是以Integer的緩存是為了減少記憶體占用,提高程式運作的效率而設計的。
進一步了解IntegerCache的源碼,我們還可以知道緩存的區間其實是可以設定的:
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
// 最小值固定為-128
static final int low = -128;
static final int high;
static final Integer cache[];
// 初始化緩存數組
static {
// 最大值可以通過屬性來配置
int h = 127;
// 擷取系統配置裡設定的值
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// 最大的緩存數組大小為Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// 如果屬性值不能轉換為int,就忽略它.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
// 為緩存數組指派
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
從注釋中我們可以得知:
- 緩存是為了支援自動裝箱對象的辨別語義,在自動裝箱時可以複用這些緩存的對象,而 -128 到 127 的範圍是基于JLS的要求而設定的
- 首次使用時會初始化緩存。緩存的大小可以通過虛拟機參數
來控制,在VM初始化期間,java.lang.Integer.IntegerCache.high可以設定并儲存在sun.misc.VM類中的私有系統屬性中(-XX:AutoBoxCacheMax=<size>}
),未指定則為 127-Djava.lang.Integer.IntegerCache.high=<value>
更多細節可以參考 JLS 的
Boxing Conversion 部分的相關描述。
3 Long
3.1 源碼分析
有了上文對于Integer類的分析,Long内部的自動裝箱、拆箱以及緩存機制也就大同小異了:
/**
* Returns a {@code Long} instance representing the specified
* {@code long} value.
* If a new {@code Long} instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Long(long)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* Note that unlike the {@linkplain Integer#valueOf(int)
* corresponding method} in the {@code Integer} class, this method
* is <em>not</em> required to cache values within a particular
* range.
*
* @param l a long value.
* @return a {@code Long} instance representing {@code l}.
* @since 1.5
*/
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
與Integer不同的是,Long的valueOf()方法并沒有被要求緩存特定範圍的值,而且通過LongCache我們可以得知Long的緩存範圍被固定在了 -128 到 127 ,并不能通過參數來修改緩存的範圍:
private static class LongCache {
private LongCache(){}
// 緩存,範圍從 -128 到 127,+1 是因為有個 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
// 緩存 Long 值,注意這裡是 i - 128 ,是以在拿的時候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
3.2 面試題
為什麼使用 Long 時,大家推薦多使用 valueOf 方法,少使用 parseLong 方法?
因為 Long 本身有緩存機制,緩存了 -128 到 127 範圍内的 Long,valueOf 方法會從緩存中去拿值,如果命中緩存,會減少資源的開銷,parseLong 方法就沒有這個機制。
4 總結
通過本文,我們學習了Integer、Long的自動裝箱、拆箱原理,還學習了它們内部的緩存機制,緩存是一種重要且常見的性能提升手段,在很多應用場景諸如Spring、資料庫、Redis中都有廣泛運用,是以了解它的機制對于我們的學習工作都有很大的幫助。本文分析了Integer、Long内部的一些巧妙設計,對于其他包裝類來說也是互通的,本文就不再贅述了,有興趣的讀者可以自行去研究。
注:以上分析皆基于JDK 1.8版本來進行。