包裝類
我們知道,Java中包含了8種基本數據類型:
- 整數類型:byte、short、int、long
- 字符類型:char
- 浮點類型:float、double
- 布爾類型:boolean
這8種基本數據類型的變量不需要使用new
來創建,它們不會在堆上創建,而是直接在棧內存中存儲,因此會比使用對象更加高效。
但是,在某些時候,基本數據類型會有一些制約,例如當有個方法需要Object類型的參數,但實際需要的值卻是2、3等數值,這就比較難以處理了。因為,所有引用類型的變量都繼承了Object類,都可當成Object類型變量使用,但基本數據類型的變量就不可以了。
為了解決這個問題,Java為這8種基本數據類型分別定義了相應的引用類型,並稱之為基本數據類型的包裝類(Wrapper Class)。包裝類均位於java.lang包下,其和基本數據類型的對應關系如下表所示:
基本數據類型 | 包裝類 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
從上表可以看出,除了int和char有點例外之外,其他的基本數據類型對應的包裝類都是將其首字母大寫。
自動裝箱和自動拆箱
在Java SE5之前,把基本數據類型變量變成包裝類實例需要通過對應包裝類的構造器來實現,即:
Integer i = new Integer(10);
把包裝器類型轉換為基本數據類型需要這樣:
int a = i.intValue();
上面的基本數據類型與包裝類對象之間的轉換有點繁瑣,所以從Java SE5開始,為了簡化開發,Java提供了自動裝箱(Autoboxing)和自動拆箱(AutoUnboxing)功能。
所謂自動裝箱,就是自動將基本數據類型轉換為包裝器類型;自動拆箱,就是自動將包裝器類型轉換為基本數據類型,下面代碼演示自動裝箱、拆箱。
// 自動裝箱
Integer i = 10;
// 自動拆箱
int a = i;
我們可以看到,當JDK提供了自動裝箱和自動拆箱功能后,大大簡化了我們的開發。
需要注意的是,進行自動裝箱和自動拆箱時必須注意類型匹配。例如,Integer只能自動拆箱成int,int只能自動裝箱成Integer。
原理
通過前文,我們已經知道了什么是包裝類及什么是自動裝、拆箱。
那么接下來,我們來看一下自動拆、裝箱的原理。有如下代碼:
public static void main(String[] args) {
Integer i = 10;// 自動裝箱
int a = i;// 自動拆箱
}
對以上代碼進行反編譯,得到如下代碼:
public static void main(String[] args) {
Integer i = Integer.valueOf(10);
int a = integer.intValue();
}
從反編譯后得到的代碼可以看出,在裝箱的時候自動調用的是Integer的valueOf(int i)
方法,而在拆箱的時候自動調用的是Integer的intValue()
方法。
其他的也類似,比如Double、Character。感興趣的同學,可以自己嘗試一下。
因此可以用一句話總結裝箱和拆箱的實現過程:
自動裝箱,都是通過包裝類的
valueOf()
方法來實現的。自動拆箱,都是通過包裝類對象的
xxxValue()
來實現的。
使用場景
我們了解過原理之后,在來看一下,什么情況下,Java會幫我們進行自動拆裝箱。前面提到的變量的初始化和賦值的場景就不介紹了,那是最簡單的也最容易理解的。
我們主要來看一下,那些可能被忽略的場景
場景1、將基本數據類型放入集合類
List<Integer> list = new ArrayList<>();
list.add(1);
當我們把基本數據類型放入集合類中的時候,代碼沒有報錯,很明顯,這里發生了自動裝箱。
將上面代碼進行反編譯,也印證了這一點:
List<Integer> list = new ArrayList<>();
list.add(Integer.valueOf(1));
場景2、包裝類型和基本類型的大小比較
Integer i = 10;
// 輸出true
System.out.println("10的包裝類實例是否大於8?" + (i > 8));
反編譯上面代碼:
Integer i = Integer.valueOf(10);
System.out.println("10的包裝類實例是否大於8?" + (i.intValue() > 8));
可以看到,當包裝類與基本數據類型進行比較運算時,是先將包裝類進行拆箱成基本數據類型,然后進行比較的。
場景3、包裝類型的運算
Integer i = 10;
Integer j = 20;
// 輸出30
System.out.println(i + j);
反編譯上面代碼:
Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(20);
System.out.println(i.intValue() + j.intValue());
可以看到,兩個包裝類型之間的運算,會被自動拆箱成基本類型進行。
場景4、三目運算符的使用
boolean flag = true;
Integer i = 0;
int j = 1;
int k = flag ? i : j;
很多人不知道,其實在第四行,會發生自動拆箱,反編譯后代碼如下:
boolean flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;
這其實是三目運算符的語法規范:當第二,第三位操作數分別為基本類型和對象時,其中的對象就會拆箱為基本類型進行操作。如果這個時候i的值為null
,那么就會發生NPE,這一點,是我們日常開發過程中的大坑。
觀察以下代碼:
public static void main(String[] args) {
Map<String, Boolean> map = new HashMap<>();
Boolean b = map != null ? map.get("test") : false;
System.out.println(b);
}
一般情況下,我們會認為以上代碼Boolean b的最終得到的值應該是null
。因為map.get("test")
的值是null
,而b又是一個對象,所以得到結果會是null
。
但是,以上代碼會拋出NPE:
Exception in thread "main" java.lang.NullPointerException
這是因為,map.get("test") == null
,當用null
去調用intValue()
方法時,拋出了NPE。
Integer的緩存機制
有如下代碼,你知道輸出結果是什么嗎?
public static void main(String[] args) {
Integer a = 1;
Integer b = 1;
Integer c = 128;
Integer d = 128;
System.out.println(a == b);
System.out.println(c == d);
}
我們都知道在Java里,當用==
來比較兩個對象時,比較的是地址,如果兩個對象引用指向堆中的同一塊內存就返回true
,否則返回false
。這一點是完全正確的。
那按照這個理論,上面代碼應該輸出都是false
,因為4個變量都是Integer
類型的對象,但實際輸出結果卻是這樣的:
true
false
這讓人疑惑:同樣是兩個int
類型的數值自動裝箱成Integer
對象,如果是兩個2自動裝箱后就相等;但如果是兩個128自動裝箱后就不相等,這是為什么呢?
一起來找下這個問題的答案:
Integer a = 1
,根據前文我們知道,這里發生了自動裝箱,而自動裝箱其實就是調用了Integer
的valueOf
方法- 既然調用了這個
valueOf
方法,那我們是不是應該去看看這個方法里到底做了什么事情? - 最后去查看Integer這個包裝類的
valueOf
的具體實現。
來,一起來看JDK里valueOf
方法的源代碼:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
方法實現很簡單,先解釋一下,IntegerCache
是Integer
類中定義的一個private static
的內部類,它維護了一個Integer
數組cache
,IntegerCache
源碼如下:
/**
* Cache to support the object identity semantics of autoboxing for values
* between -128 and 127 (inclusive) as required by JLS.
* <p>
* 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 {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
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);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
} catch (NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
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 Integer.IntegerCache.high >= 127;
}
private IntegerCache() {
}
}
從上面源碼可以看出:
- IntegerCache類有一個int類型的常量low,值為-128
- 還有一個int類型的常量high,它的值通過靜態初始化塊進行賦值,默認為127(從javadoc可以看到,這個值可以通過虛擬機參數:-XX:AutoBoxCacheMax=
進行設置) - 最后,還有一個Integer類型的數組cache[],它同樣通過靜態初始化塊進行初始化,長度==(high - low) + 1,默認為(127+128+1)=256,數組元素值為-128~127。
- ps:大佬們寫的代碼真的是太優雅了!!
我們終於找到上面問題的答案了:
Integer類初始化時,會把一個-128127之間的Integer類型對象放入一個名為cache的數組中**緩存**起來。如果以后把一個-128127之間的基本數據類型自動裝箱成一個Integer實例時(即調用valueOf方法),實際上是直接引用了cache數組中的對應元素。但每次把一個不在-128~127范圍內的整數自動裝箱成Integer實例時,就需要重新new
一個Integet實例,所以出現了上面那樣的運行結果。
緩存是一種非常優秀的設計模式,在Java、JavaEE平台的很多地方都會通過緩存來提高系統的性能。
類型的,Byte、Short、Long、Character也有相同的緩存機制,值得注意的是Double、Float是沒有緩存機制的。有興趣的同學,可以自行查看源碼。
總結
- 裝箱操作會創建對象,頻繁的裝箱操作會消耗許多內存,影響性能,所以可以避免裝箱的時候應該盡量避免。
- 有些場景會進行自動拆裝箱,此時要注意包裝類對象是否為
null
,否則自動拆箱時就有可能拋出NPE。 - 包裝對象的數值比較,不能簡單的使用
==
,雖然-128到127之間的數字可以,但是這個范圍之外還是需要使用equals
比較。