(超詳細(xì))Java自動(dòng)裝箱拆箱

詳解自動(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ù)類型,下面是裝箱和拆箱的類型。

20160329101454749.jpg
20150922151443893.jpg

這個(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為例,來分析一下它的源碼:

  1. 首先來看看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)存的消耗,影響性能。

  2. 接著看看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é),如下圖:

20150922153039509.jpg

下面我們來看看另外一種情況:

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


20150922153446481.jpg

可以看到運(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)文章提到的問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容