Java SE 6 技術手冊

4.2 自動裝箱、拆箱

基本(Primitive)型態的自動裝箱(autoboxing)、拆箱(unboxing)是 J2SE 5.0 提供的新功能,雖然提供了您不用自行包裹基本型態的方便性,但提供方便的同時表示隱藏了細節,建議您在能夠區分基本型態與物件的差別時再來使用。

4.2.1 autoboxing、 unboxing

在 Java 中,所有您要處理的東西「幾乎」都是物件(Object),例如您之前所使用的 Scanner 是物件,字串(String)也是物件,您之後還會看到更多的物件,然而基本(Primitive)資料型態不是物件,也就是您使用 int、double、boolean 等宣告的變數,以及您在程式中直接寫下的字面常量。

在前一個小節中您已經大致看過操作物件的方便性,而使用Java有一段時間的人都知道,有些時候您需要將基本型態轉換為物件,例如使用Map物件要操作put()方法時,需要傳入的引數是物件而不是基本型態。

您要使用包裹型態(Wrapper Types)才能將基本資料型態包裝為物件,前一個小節中您已經知道在 J2SE 5.0 之前,要如下才能將 int 包裝為一個 Integer 物件:

Integer integer = new Integer(10);

在 J2SE 5.0 之後提供了自動裝箱的功能,您可以直接如下撰寫來包裹基本型態:

Integer integer = 10;

在進行編譯時,編譯器在自動根據您寫下的陳述,判斷是否為您進行自動裝箱動作,在上例中您的 integer 參考的會是 Integer 類別的實例;同樣的動作可以適用於 boolean、byte、short、char、long、float、double 等基本型態,分別會使用對應的包裹型態(Wrapper Types)Boolean、Byte、Short、Character、Integer、Long、Float或Double;直接使用自動裝箱功能來改寫一下範例 4.4 作為練習。

範例 4.5 AutoBoxDemo.java

public class AutoBoxDemo {
    public static void main(String[] args) {
        Integer data1 = 10;
        Integer data2 = 20;

        // 轉為double值再除以3
        System.out.println(data1.doubleValue() / 3);

        // 進行兩個值的比較
        System.out.println(data1.compareTo(data2));
    }
}

程式看來簡潔了許多,data1與data2在運行時就是Integer的實例,可以直接進行物件操作,執行的結果如下:

3.3333333333333335
-1

自動裝箱運用的方法還可以如下:

int i = 10; 
Integer integer = i;

您也可以使用更一般化的 java.lang.Number 類別來自動裝箱,例如:

Number number = 3.14f;

3.14f 會先被自動裝箱為 Float,然後指定給 number。

J2SE 5.0 中可以自動裝箱,也可以自動拆箱(unboxing),也就是將物件中的基本形態資訊從物件中自動取出,例如下面這樣寫是可以的:

Integer fooInteger = 10;
int fooPrimitive = fooInteger;

fooInteger 參考至自動裝箱為 Integer 的實例後,如果被指定給一個 int 型態的變數 fooPrimitive,則會自動變為 int 型態再指定給 fooPrimitive;在運算時,也可以進行自動裝箱與拆箱,例如:

Integer i = 10;
System.out.println(i + 10);
System.out.println(i++);

上例中會顯示 20 與 10,編譯器會自動幫您進行自動裝箱與拆箱,也就是 10 會先被裝箱,然後在 i + 10 時會先拆箱,進行加法運算;i++ 該行也是先拆箱再進行遞增運算。再來看一個例子:

Boolean boo = true;
System.out.println(boo && false);

同樣的 boo 原來是 Boolean 的實例,在進行 AND 運算時,會先將 boo 拆箱,再與 false 進行 AND 運算,結果會顯示 false。

4.2.2 小心使用 boxing

自動裝箱與拆箱的功能事實上是編譯器來幫您的忙,編譯器在編譯時期依您所撰寫的語法,決定是否進行裝箱或拆箱動作,例如:

Integer i = 100;

相當於編譯器自動為您作以下的語法編譯:

Integer i = new Integer(100);

所以自動裝箱與拆箱的功能是所謂的「編譯器蜜糖」(Compiler sugar),雖然使用這個功能很方便,但在程式運行階段您還是瞭解 Java 的語義,例如下面的程式是可以通過編譯的:

Integer i = null;
int j = i;

這樣的語法在編譯時期是合法的,但是在運行時期會有錯誤,因為這種寫法相當於:

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

null 表示 i 沒有參考至任何的物件實體,它可以合法的指定給物件參考名稱,由於實際上 i 並沒有參考至任何的物件,所以也就不可能操作 intValue() 方法,所以上面的寫法在運行時會出現 NullPointerException 的錯誤。

自動裝箱、拆箱的功能提供了方便性,但隱藏了一些細節,所以必須小心,再來看範例 4.6,您以為結果是如何?

範例 4.6 AutoBoxDemo2.java

public class AutoBoxDemo2 {
    public static void main(String[] args) {
        Integer i1 = 100;
        Integer i2 = 100;

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

以自動裝箱與拆箱的機制來看,您可能會覺得結果是顯示 "i1 == i2",您是對的!那麼範例 4.7 的這個程式,您覺得結果是什麼?

範例 4.7 AutoBoxDemo3.java

public class AutoBoxDemo3 {
    public static void main(String[] args) {
        Integer i1 = 200;
        Integer i2 = 200;

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

結果是顯示 "i1 != i2",這有些令人訝異,兩個範例語法完全一樣,只不過改個數值而已,結果卻相反。

其實這與 '==' 運算子的比較有關,在第 3 章中介紹過 '==' 是用來比較兩個基本型態的變數值是否相等,事實上 '==' 也用於判斷兩個物件參考名稱是否參考至同一個物件。

在自動裝箱時對於值從 -128 到 127 之間的值,它們被裝箱為 Integer 物件後,會存在記憶體之中被重用,所以範例 4.6 中使用 '==' 進行比較時,i1 與 i2 實際上參考至同一個物件,如果超過了從 -128 到 127 之間的值,被裝箱後的 Integer 物件並不會被重用,即相當於每次裝箱時都新建一個 Integer 物件,所以範例 4.7 使用 '==' 進行比較時,i1 與 i2 參考的是不同的物件。

所以不要過份依賴自動裝箱與拆箱,您還是必須知道基本型態與物件的差異,範例 4.7 最好還是依正規的方式來寫,而不是依賴編譯器蜜糖(Compiler sugar),例如範例 4.7 必須改寫為範例 4.8 才是正確的。

範例 4.8 AutoBoxDemo4.java

public class AutoBoxDemo4 {
    public static void main(String[] args) {
        Integer i1 = 200;
        Integer i2 = 200;

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

結果這次是顯示 "i1 == i2" 了,使用這樣的寫法,相信您也會比較放心一些,對於這些方便但隱藏細節的功能到底要不要用呢?基本上只有一個原則:如果您不確定就不要用。

良葛格的話匣子 基本上我是建議新手不要使用自動裝箱、拆箱的語法,在這邊說明這個功能是為了要完整性介紹 J2SE 5.0 的特性,新手入門的話,最好在對物件有較深入瞭解之後,再來使用這個功能。