詳解自動(dòng)拆箱與自動(dòng)裝箱
一、 什么是自動(dòng)裝箱、自動(dòng)拆箱
// 自動(dòng)裝箱
Integer total1 = 99;
// 自動(dòng)拆箱
int total2 = total1;
簡(jiǎn)單一點(diǎn)說,裝箱就是自動(dòng)將基本數(shù)據(jù)類型轉(zhuǎn)換為包裝器類型,拆箱就是自動(dòng)將包裝器類型轉(zhuǎn)換為基本數(shù)據(jù)類型,下面是裝箱和拆箱的類型。


這個(gè)過程是自動(dòng)執(zhí)行的,那么我們需要看看它的執(zhí)行過程:
public class Main{
psvm(String[] args) {
// 自動(dòng)裝箱
Integer total1 = 99;
// 自動(dòng)拆箱
int total2 = total1;
}
}
反編譯class文件之后,詳解每一行的代碼發(fā)現(xiàn):
Integer total1 = 99;
// 執(zhí)行如上代碼時(shí)系統(tǒng)自動(dòng)為我們執(zhí)行了:
Integer total1 = Integer.valueOf(99);
int total2 = total1;
// 執(zhí)行如上代碼時(shí)系統(tǒng)自動(dòng)為我們執(zhí)行了:
int total2 = total1.intValue();
我們現(xiàn)在就以Integer為例,來分析一下它的源碼:
-
首先來看看
Integer.valueOf函數(shù)public static Integer valueOf(int i) { return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128]; }它首先會(huì)判斷i的大?。喝绻鹖小于-128或者大于等于128,就創(chuàng)建一個(gè)Integer對(duì)象,否則執(zhí)行SMALL_VALUES[I + 128]。
- 對(duì)于Integer對(duì)象:
private final int value; public Integer(int value) { this.value = value; } public Integer(String string) throws NumberFormatException { this(parseInt(string)); }它里面定義了一個(gè)value變量,創(chuàng)建一個(gè)Integer對(duì)象,就會(huì)給這個(gè)變量初始化。第二個(gè)傳入的是一個(gè)String類型的變量,它會(huì)先把它轉(zhuǎn)換成int值,然后進(jìn)行初始化。
-
對(duì)于SMALL_VALUES[i + 128]
private static final Integer[] SMALL_VALUES = new Integer[256];它是一個(gè)靜態(tài)的Integer數(shù)組對(duì)象,也就是說最終valueOf返回的都是一個(gè)Integer對(duì)象。所以我們這里可以總結(jié)一點(diǎn):裝箱的過程中會(huì)創(chuàng)建對(duì)應(yīng)的像,這個(gè)會(huì)消耗內(nèi)存,所以裝箱的過程會(huì)增加內(nèi)存的消耗,影響性能。
-
接著看看intValue函數(shù)
@Override public int intValue() { return value; }這個(gè)很簡(jiǎn)單,直接返回value值即可。
二、 相關(guān)問題
上面我們看到在Integer的構(gòu)造函數(shù)中,它分兩種情況:
1、i >= 128 || i < -128 =====> new Integer(i)
2、i < 128 && i >= -128 =====> SMALL_VALUES[i + 128]
SMALL_VALUES本來已經(jīng)被創(chuàng)建好,也就是說在i >= 128 || i < -128是會(huì)創(chuàng)建不同的對(duì)象,在i < 128 && i >= -128會(huì)根據(jù)i的值返回已經(jīng)創(chuàng)建好的指定的對(duì)象。
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2); //true
System.out.println(i3==i4); //false
}
}
代碼的后面,我們可以看到它們的執(zhí)行結(jié)果是不一樣的,為什么,在看看我們上面的說明。
1、i1和i2會(huì)進(jìn)行自動(dòng)裝箱,執(zhí)行了valueOf函數(shù),它們的值在(-128,128]這個(gè)范圍內(nèi),它們會(huì)拿到SMALL_VALUES數(shù)組里面的同一個(gè)對(duì)象SMALL_VALUES[228],它們引用到了同一個(gè)Integer對(duì)象,所以它們肯定是相等的。
2、i3和i4也會(huì)進(jìn)行自動(dòng)裝箱,執(zhí)行了valueOf函數(shù),它們的值大于128,所以會(huì)執(zhí)行new Integer(200),也就是說它們會(huì)分別創(chuàng)建兩個(gè)不同的對(duì)象,所以它們肯定不等。
下面我們來看看另外一個(gè)例子:
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2); //false
System.out.println(i3==i4); //false
}
}
看看上面的執(zhí)行結(jié)果,跟Integer不一樣,這樣也不必奇怪,因?yàn)樗鼈兊膙alueOf實(shí)現(xiàn)不一樣,結(jié)果肯定不一樣,那為什么它們不統(tǒng)一一下呢?
這個(gè)很好理解,因?yàn)閷?duì)于Integer,在(-128,128]之間只有固定的256個(gè)值,所以為了避免多次創(chuàng)建對(duì)象,我們事先就創(chuàng)建好一個(gè)大小為256的Integer數(shù)組SMALL_VALUES,所以如果值在這個(gè)范圍內(nèi),就可以直接返回我們事先創(chuàng)建好的對(duì)象就可以了。
但是對(duì)于Double類型來說,我們就不能這樣做,因?yàn)樗谶@個(gè)范圍內(nèi)個(gè)數(shù)是無限的。
總結(jié)一句就是:在某個(gè)范圍內(nèi)的整型數(shù)值的個(gè)數(shù)是有限的,而浮點(diǎn)數(shù)卻不是。
所以在Double里面的做法很直接,就是直接創(chuàng)建一個(gè)對(duì)象,所以每次創(chuàng)建的對(duì)象都不一樣。
下面我們進(jìn)行一個(gè)歸類:
Integer派別:Integer、Short、Byte、Character、Long這幾個(gè)類的valueOf方法的實(shí)現(xiàn)是類似的。
Double派別:Double、Float的valueOf方法的實(shí)現(xiàn)是類似的。每次都返回不同的對(duì)象。
下面對(duì)Integer派別進(jìn)行一個(gè)總結(jié),如下圖:

下面我們來看看另外一種情況:
public class Main {
public static void main(String[] args) {
Boolean i1 = false;
Boolean i2 = false;
Boolean i3 = true;
Boolean i4 = true;
System.out.println(i1==i2);//true
System.out.println(i3==i4);//true
}
}
可以看到返回的都是true,也就是它們執(zhí)行valueOf返回的都是相同的對(duì)象。
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
可以看到它并沒有創(chuàng)建對(duì)象,因?yàn)樵趦?nèi)部已經(jīng)提前創(chuàng)建好兩個(gè)對(duì)象,因?yàn)樗挥袃煞N情況,這樣也是為了避免重復(fù)創(chuàng)建太多的對(duì)象。
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
上面把幾種情況都介紹到了,下面來進(jìn)一步討論其他情況。
Integer num1 = 400;
int num2 = 400;
System.out.println(num1 == num2); //true
說明num1 == num2進(jìn)行了拆箱操作
Integer num1 = 100;
int num2 = 100;
System.out.println(num1.equals(num2)); //true
我們先來看看equals源碼:
@Override
public boolean equals(Object o) {
return (o instanceof Integer) && (((Integer) o).value == value);
}
我們指定equal比較的是內(nèi)容本身,并且我們也可以看到equal的參數(shù)是一個(gè)Object對(duì)象,我們傳入的是一個(gè)int類型,所以首先會(huì)進(jìn)行裝箱,然后比較,之所以返回true,是由于它比較的是對(duì)象里面的value值。
Integer num1 = 100;
int num2 = 100;
Long num3 = 200l;
System.out.println(num1 + num2); //200
System.out.println(num3 == (num1 + num2)); //true
System.out.println(num3.equals(num1 + num2)); //false
1、當(dāng)一個(gè)基礎(chǔ)數(shù)據(jù)類型與封裝類進(jìn)行==、+、-、*、/運(yùn)算時(shí),會(huì)將封裝類進(jìn)行拆箱,對(duì)基礎(chǔ)數(shù)據(jù)類型進(jìn)行運(yùn)算。
2、對(duì)于num3.equals(num1 + num2)為false的原因很簡(jiǎn)單,我們還是根據(jù)代碼實(shí)現(xiàn)來說明:
@Override
public boolean equals(Object o) {
return (o instanceof Long) && (((Long) o).value == value);
}
它必須滿足兩個(gè)條件才為true:
1、類型相同
2、內(nèi)容相同
上面返回false的原因就是類型不同。
Integer num1 = 100;
Integer num2 = 200;
Long num3 = 300l;
System.out.println(num3 == (num1 + num2)); //true
我們來反編譯一些這個(gè)class文件:javap -c StringTest

可以看到運(yùn)算的時(shí)候首先對(duì)num3進(jìn)行拆箱(執(zhí)行num3的longValue得到基礎(chǔ)類型為long的值300),然后對(duì)num1和mum2進(jìn)行拆箱(分別執(zhí)行了num1和num2的intValue得到基礎(chǔ)類型為int的值100和200),然后進(jìn)行相關(guān)的基礎(chǔ)運(yùn)算。
我們來對(duì)基礎(chǔ)類型進(jìn)行一個(gè)測(cè)試:
int num1 = 100;
int num2 = 200;
long mum3 = 300;
System.out.println(num3 == (num1 + num2)); //true
就說明了為什么最上面會(huì)返回true.
所以,當(dāng) “==”運(yùn)算符的兩個(gè)操作數(shù)都是 包裝器類型的引用,則是比較指向的是否是同一個(gè)對(duì)象,而如果其中有一個(gè)操作數(shù)是表達(dá)式(即包含算術(shù)運(yùn)算)則比較的是數(shù)值(即會(huì)觸發(fā)自動(dòng)拆箱的過程)。
陷阱1:
Integer integer100=null;
int int100=integer100;
這兩行代碼是完全合法的,完全能夠通過編譯的,但是在運(yùn)行時(shí),就會(huì)拋出空指針異常。其中,integer100為Integer類型的對(duì)象,它當(dāng)然可以指向null。但在第二行時(shí),就會(huì)對(duì)integer100進(jìn)行拆箱,也就是對(duì)一個(gè)null對(duì)象執(zhí)行intValue()方法,當(dāng)然會(huì)拋出空指針異常。所以,有拆箱操作時(shí)一定要特別注意封裝類對(duì)象是否為null。
總結(jié):
1、需要知道什么時(shí)候會(huì)引發(fā)裝箱和拆箱
2、裝箱操作會(huì)創(chuàng)建對(duì)象,頻繁的裝箱操作會(huì)消耗許多內(nèi)存,影響性能,所以可以避免裝箱的時(shí)候應(yīng)該盡量避免。
3、equals(Object o) 因?yàn)樵璭quals方法中的參數(shù)類型是封裝類型,所傳入的參數(shù)類型(a)是原始數(shù)據(jù)類型,所以會(huì)自動(dòng)對(duì)其裝箱,反之,會(huì)對(duì)其進(jìn)行拆箱
4、當(dāng)兩種不同類型用==比較時(shí),包裝器類的需要拆箱, 當(dāng)同種類型用==比較時(shí),會(huì)自動(dòng)拆箱或者裝箱
三、何時(shí)發(fā)生自動(dòng)拆裝箱
自動(dòng)裝箱和拆箱在Java中很常見,比如我們有一個(gè)方法,接受一個(gè)對(duì)象類型的參數(shù),如果我們傳遞一個(gè)原始類型值,那么Java會(huì)自動(dòng)講這個(gè)原始類型值轉(zhuǎn)換成與之對(duì)應(yīng)的對(duì)象。最經(jīng)典的一個(gè)場(chǎng)景就是當(dāng)我們向ArrayList這樣的容器中增加原始類型數(shù)據(jù)時(shí)或者是創(chuàng)建一個(gè)參數(shù)化的類,比如下面的ThreadLocal。
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(1); //autoboxing - primitive to object
intList.add(2); //autoboxing
ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>();
intLocal.set(4); //autoboxing
int number = intList.get(0); // unboxing
int local = intLocal.get(); // unboxing in Java
還有一種情況就是Byte Short Int Long Float Double Character 這七種包裝類在互相進(jìn)行運(yùn)算時(shí)會(huì)先各自拆箱,然后遵循自動(dòng)類型提升的原則再進(jìn)行運(yùn)算。
Object o1 = true ? new Integer(1) : new Double(2.0);
sout(o1); // 1.0
以上結(jié)果輸出應(yīng)該是1.0,分析代碼,首先是一個(gè)三元表達(dá)式的運(yùn)算,表達(dá)式的兩邊屬于以上7種包裝類,編譯時(shí)首先進(jìn)行拆箱,拆箱為int類型和double類型,兩種類型運(yùn)算時(shí)符合自動(dòng)類型提升的規(guī)則,故int類型的1提升至double類型的1.0 再將double的1.0賦值給Object類型的o1時(shí),會(huì)進(jìn)行裝箱的操作,故o1最后指向的是一個(gè)Double類型的對(duì)象,在打印o1時(shí),由于多態(tài)性的體現(xiàn)會(huì)執(zhí)行Double類型重寫Object的toString方法,所以會(huì)打印出來Double的值1.0,而不是地址,或整型數(shù)1等其他內(nèi)容。
舉例說明
上面的部分我們介紹了自動(dòng)裝箱和拆箱以及它們何時(shí)發(fā)生,我們知道了自動(dòng)裝箱主要發(fā)生在兩種情況,一種是賦值時(shí),另一種是在方法調(diào)用的時(shí)候。為了更好地理解這兩種情況,我們舉例進(jìn)行說明。
賦值時(shí)
這是最常見的一種情況,在Java 1.5以前我們需要手動(dòng)地進(jìn)行轉(zhuǎn)換才行,而現(xiàn)在所有的轉(zhuǎn)換都是由編譯器來完成。
//before autoboxing
Integer iObject = Integer.valueOf(3);
Int iPrimitive = iObject.intValue()
//after java5
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
方法調(diào)用時(shí)
這是另一個(gè)常用的情況,當(dāng)我們?cè)诜椒ㄕ{(diào)用時(shí),我們可以傳入原始數(shù)據(jù)值或者對(duì)象,同樣編譯器會(huì)幫我們進(jìn)行轉(zhuǎn)換。
public static Integer show(Integer iParam){
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}
//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer
show方法接受Integer對(duì)象作為參數(shù),當(dāng)調(diào)用show(3)時(shí),會(huì)將int值轉(zhuǎn)換成對(duì)應(yīng)的Integer對(duì)象,這就是所謂的自動(dòng)裝箱,show方法返回Integer對(duì)象,而int result = show(3);中result為int類型,所以這時(shí)候發(fā)生自動(dòng)拆箱操作,將show方法的返回的Integer對(duì)象轉(zhuǎn)換成int值。
四、自動(dòng)裝箱的弊端
自動(dòng)裝箱有一個(gè)問題,那就是在一個(gè)循環(huán)中進(jìn)行自動(dòng)裝箱操作的情況,如下面的例子就會(huì)創(chuàng)建多余的對(duì)象,影響程序的性能。
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}
上面的代碼sum+=i可以看成sum = sum + i,但是+這個(gè)操作符不適用于Integer對(duì)象,首先sum進(jìn)行自動(dòng)拆箱操作,進(jìn)行數(shù)值相加操作,最后發(fā)生自動(dòng)裝箱操作轉(zhuǎn)換成Integer對(duì)象。其內(nèi)部變化如下
int result = sum.intValue() + i;
Integer sum = new Integer(result);
由于我們這里聲明的sum為Integer類型,在上面的循環(huán)中會(huì)創(chuàng)建將近4000個(gè)無用的Integer對(duì)象,在這樣龐大的循環(huán)中,會(huì)降低程序的性能并且加重了垃圾回收的工作量。因此在我們編程時(shí),需要注意到這一點(diǎn),正確地聲明變量類型,避免因?yàn)樽詣?dòng)裝箱引起的性能問題。
五、注意事項(xiàng)
自動(dòng)裝箱和拆箱可以使代碼變得簡(jiǎn)潔,但是其也存在一些問題和極端情況下的問題,以下幾點(diǎn)需要我們加強(qiáng)注意。
1. 對(duì)象相等比較
這是一個(gè)比較容易出錯(cuò)的地方,”==“可以用于原始值進(jìn)行比較,也可以用于對(duì)象進(jìn)行比較,當(dāng)用于對(duì)象與對(duì)象之間比較時(shí),比較的不是對(duì)象代表的值,而是檢查兩個(gè)對(duì)象是否是同一對(duì)象,這個(gè)比較過程中沒有自動(dòng)裝箱發(fā)生。進(jìn)行對(duì)象值比較不應(yīng)該使用”==“,而應(yīng)該使用對(duì)象對(duì)應(yīng)的equals方法??匆粋€(gè)能說明問題的例子。
public class AutoboxingTest {
public static void main(String args[]) {
// Example 1: == comparison pure primitive – no autoboxing
int i1 = 1;
int i2 = 1;
System.out.println("i1==i2 : " + (i1 == i2)); // true
// Example 2: equality operator mixing object and primitive
Integer num1 = 1; // autoboxing
int num2 = 1;
System.out.println("num1 == num2 : " + (num1 == num2)); // true
// Example 3: special case - arises due to autoboxing in Java
Integer obj1 = 1; // autoboxing will call Integer.valueOf()
Integer obj2 = 1; // same call to Integer.valueOf() will return same
// cached Object
System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true
// Example 4: equality operator - pure object comparison
Integer one = new Integer(1); // no autoboxing
Integer anotherOne = new Integer(1);
System.out.println("one == anotherOne : " + (one == anotherOne)); // false
}
}
Output:
i1==i2 : true
num1 == num2 : true
obj1 == obj2 : true
one == anotherOne : false
值得注意的是第三個(gè)小例子,這是一種極端情況。obj1和obj2的初始化都發(fā)生了自動(dòng)裝箱操作。但是處于節(jié)省內(nèi)存的考慮,JVM會(huì)緩存-128到127的Integer對(duì)象。因?yàn)閛bj1和obj2實(shí)際上是同一個(gè)對(duì)象。所以使用”==“比較返回true。
容易混亂的對(duì)象和原始數(shù)據(jù)值
另一個(gè)需要避免的問題就是混亂使用對(duì)象和原始數(shù)據(jù)值,一個(gè)具體的例子就是當(dāng)我們?cè)谝粋€(gè)原始數(shù)據(jù)值與一個(gè)對(duì)象進(jìn)行比較時(shí),如果這個(gè)對(duì)象沒有進(jìn)行初始化或者為Null,在自動(dòng)拆箱過程中obj.xxxValue,會(huì)拋出NullPointerException,如下面的代碼
private static Integer count;
//NullPointerException on unboxing
if( count <= 0){
System.out.println("Count is not started yet");
}
緩存的對(duì)象
這個(gè)問題就是我們上面提到的極端情況,在Java中,會(huì)對(duì)-128到127的Integer對(duì)象進(jìn)行緩存,當(dāng)創(chuàng)建新的Integer對(duì)象時(shí),如果符合這個(gè)這個(gè)范圍,并且已有存在的相同值的對(duì)象,則返回這個(gè)對(duì)象,否則創(chuàng)建新的Integer對(duì)象。在Java中另一個(gè)節(jié)省內(nèi)存的例子就是字符串常量池,感興趣的同學(xué)可以了解一下。
生成無用對(duì)象增加GC壓力
因?yàn)樽詣?dòng)裝箱會(huì)隱式地創(chuàng)建對(duì)象,像前面提到的那樣,如果在一個(gè)循環(huán)體中,會(huì)創(chuàng)建無用的中間對(duì)象,這樣會(huì)增加GC壓力,拉低程序的性能。所以在寫循環(huán)時(shí)一定要注意代碼,避免引入不必要的自動(dòng)裝箱操作。如想了解垃圾回收和內(nèi)存優(yōu)化,可以查看本文Google IO:Android內(nèi)存管理主題演講記錄
總的來說,自動(dòng)裝箱和拆箱著實(shí)為開發(fā)者帶來了很大的方便,但是在使用時(shí)也是需要格外留意,避免引起出現(xiàn)文章提到的問題。