自動裝箱、拆箱內幕


Java是個高階語言,其實是可以忽略自動裝箱、拆箱內幕,不過對初學者來說,為了能清楚區別基本型態與類別型態之不同,瞭解自動裝箱、拆箱內幕其實是有所幫助。

自動裝箱與拆箱的功能事實上是編譯器蜜糖(Compiler sugar),也就是編譯器讓你撰寫程式時吃點甜頭,編譯時期依所撰寫的語法,決定是否進行裝箱或拆箱動作。例如:

Integer i = 100;

在Oracle的JDK上,編譯器會自動將程式碼展開為:

Integer i = Integer.valueOf(100);

Java的位元碼格式也是公開標準,有位元碼檔案,就可以嘗試使用反組譯程式轉譯為Java語法。這邊使用的反組譯程式是 JAD

使用Integer.valueOf()也是為基本型態建立包裹器的方式之一。瞭解編譯器會如何裝箱與拆箱是必要的,例如下面的程式是可以通過編譯的:

Integer i = null;
int j = i;

但是在執行時期會有錯誤,因為編譯器會將之展開為:

Integer integer = null;
int i = integer.intValue();

在Java程式碼中,null代表一個特殊物件,任何類別宣告的參考名稱都可以參考至null,表示該名稱沒有參考至任何物件實體,這相當於有個名牌沒有任何人佩戴。在上例中,由於i並沒有參考至任何物件,所以就不可能操作intValue()方法,就相當於有個名牌沒有人佩戴,你卻要求戴名牌的人舉手,這是一種錯誤,在Java中會出現NullPointerException的錯誤訊息。

編譯器蜜糖通常提供了方便性,但也因此隱藏了一些細節,所以別只顧著吃糖而忽略了該知道的觀念。來看看,如果你如下撰寫,結果會是如何?

Integer i1 = 100;
Integer i2 = 100;
if (i1 == i2) {
    System.out.println("i1 == i2");
}
else {
    System.out.println("i1 != i2");
}

如果只看Integer i1 = 100,就好像在看int i1 = 100,直接使用==進行比較,有的人會理所當然回答顯示i1 == i2,那麼底下這個呢?

Integer i1 = 200;
Integer i2 = 200;
if (i1 == i2) {
    System.out.println("i1 == i2");
}
else {
    System.out.println("i1 != i2");
}

注意!程式碼只不過將100改為200,但執行結果會顯示i1 != i2,這是為何?先前提過,自動裝箱是編譯器蜜糖,以上例來說,實際上會使用Integer.valueOf()來建立Integer實例,所以你要知道Integer.valueOf()到底如何建立Integer實例,察查JDK資料夾src.zip中的java/lang資料夾中的Integer.java,你會看到valueOf()的實作內容:

public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

這段程式碼簡單來說,就是如果傳入的intIntegerCache.lowIntegerCache.high之間,那就嘗試看看先前快取(Cache)中有沒有包裹過相同的值,如果有就直接傳回,否則就使用new建構新的Integer實例。IntegerCache.low預設值是-128,IntegerCache.high預設值是127。

所以如果是這個程式碼:

Integer i1 = 100;
Integer i2 = 100;

第一行程式碼由於100在-128到127間,會從快取中傳回Integer實例,第二行程式碼執行時,要包裹的同樣是100,也是從快取中傳回同一Integer實例,所以i1i2會參考到同一個Integer實例,使用==比較就會是true

如果是這個程式碼:

Integer i1 = 200;
Integer i2 = 200;

第一行程式碼由於100不在-128到127間,所以直接建立Integer實例,第二行程式碼執行時,也是直接建立新的Integer實例,所以i1i2不會參考到同一個Integer實例,使用==比較就會是false

IntegerCache.low預設值是-128,執行時期無法更改,IntegerCache.high預設值是127,可以於啟動JVM時,使用系統屬性java.lang.Integer.IntegerCache.high來指定。例如:

> java -Djava.lang.Integer.IntegerCache.high=300 cc.openhome.Demo

如上指定之後,Integer就會針對-128到300範圍中建立的包裹器進行快取,而針對先前i1i2包裹200時,使用==比較的結果,就又顯示i1 == i2了。

所以結論就是還是一樣,別使用==!=來比較兩個物件實質內容值是否相同(因為==!=是比較物件參考),而要使用equals()。例如以下的程式碼:

Integer i1 = 200;
Integer i2 = 200;
if (i1.equals(i2)) {
    System.out.println("i1 == i2");
}
else {
    System.out.println("i1 != i2");
}

無論實際上i1i2包裹的值座落在哪個範圍,只要i1i2包裹的值相同,equals()比較的結果就會是true