8-2 自動裝箱( autoboxing )、拆箱( unboxing )

在 Java 程式設計中,常常要將基本資料型態數值包裝為Wrapper 物件,而另一方面為了進行數值計算,又常常必須從 Wrapper 物件中取得基本資料型態數值,為了程式設計人員撰寫程式時的方便,在 JDK 5.0 之後提供了自動裝箱( autoboxing )與拆箱( unboxing )的機制。

  • 認識、使用 Wrapper 類別
  • 變數與參考的差別

• 自動裝箱

如果您要將數值 10 包裝為物件,在 JDK 5.0 之前您必須這麼撰寫:

Integer iRef = new Integer(10);
                            

然而在 JDK 5.0 之後提供了自動裝箱的功能,您可以直接這麼撰寫程式:

Integer iRef = 10;
                            

對於熟悉物件與基本資料型態差別的程式開發人員來說,自動裝箱是個很方便的機制,但對於 Java 程式設計的初學者而言,由於自動裝箱的語法撰寫方式與基本型態變數宣告過於類似,所以很可能會忽略了物件的特性。

舉個實際的例子來說,來看看程式碼 8-2 會是什麼樣的執行結果。

public class AutoBoxDemo{
    public static void main(String[] args) {
        Integer iRef1 = 100;
        Integer iRef2 = 100;

        if (iRef1 == iRef2) {
            System.out.println("iRef1 == iRef2");
        }
        else {
            System.out.println("iRef1 != iRef2");
        }
    }
}
                            
程式碼 8-2 AutoBoxDemo.java

初學者以變數宣告與比較的觀點來看,會猜想結果必然是顯示"iRef1 == iRef2",實際執行結果確實是如此顯示:

圖8-9 程式碼 8-2 的執行結果

執行結果如此顯示並不是代表初學者的猜想是正確的,因為同一個猜想在程式碼 8-3 就行不通了。

public class AutoBoxDemo2{
    public static void main(String[] args) {
        Integer iRef1 = 200;
        Integer iRef2 = 200;

        if (iRef1 == iRef2) {
            System.out.println("iRef1 == iRef2");
        }
        else {
            System.out.println("iRef1 != iRef2");
        }
    }
}
                            
程式碼 8-3 AutoBoxDemo2.java

執行的結果會顯示"iRef1 != iRef2",如下所示:

圖8-10 程式碼 8-3 的執行結果

程式碼 8-3 只不過將程式碼 8-2 的第 3 行與第 4 行修改為指定值為 200,其餘部份並沒有改變,卻造成了不同的執行結果,其實這邊涉及到關係運算子"=="用於參考名稱的比較問題。

關係運算子"=="使用於兩個參考名稱之比較時,它比較的是兩個參考名稱是否參考至同一個物件,以程式碼 8-2 或8-3 為例,就是在比較 iRef1 與 iRef2 是否繫結至同一個物件。

自動裝箱機制對於整數值 -128127 之間的值,在裝箱為物件之後,會存在記憶體之中一直被重複使用,而對於-128 到 127 之外的值,則於執行時期運行到該段程式碼時,才建立一個新的物件。

所以對於程式碼 8-2 來說, iRef1 與 iRef2 由於指定的值 100 介於-128 到 127 之間,為裝箱時的重用範圍,所以 iRef1 與 iRef2 實際上繫結至同一個物件,因而第 6 行程式在進行"=="的比較運算時,結果會是 true,所以才顯示"iRef1 == iRef2"的結果。

圖8-11 程式碼 8-2 中 iRef1 與 iRef2 參考至同一個物件

然而對於程式碼 8-3 來說, iRef1 與 iRef2 由於指定的值 200 不在-128 到 127 之間,不是裝箱時的重用範圍,所以執行時 iRef1 與 iRef2 會繫結至不同物件,因而第 6 行程式在進行"=="的比較運算時,結果會是 false,所以才顯示"iRef1 != iRef2"的結果。

圖8-12 程式碼 8-3 中 iRef1 與 iRef2 參考至不同物件

結論:您不可以使用關係運算子"=="來比較兩個物件是否相等,要比較兩個物件的相等性時,您要使用 equals()方法。

equals()方法被用來定義兩個物件的相等性,對於 Integer型態的物件來說,equals()方法定義其內含值(即一開始建立 Integer 物件時所給定的值)是否相同,所以您要如程式碼 8-4 第 6 行,使用 equals()來比較物件才是正確的。

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

        if (iRef1.equals(iRef2)) {
            System.out.println("iRef1 == iRef2");
        }
        else {
            System.out.println("iRef1 != iRef2");
        }
    }
}
                            
程式碼 8-4 AutoBoxDemo3.java

執行結果如下:

圖8-13 程式碼 8-4 的執行結果

重點提示

任何時候您要比較兩個物件的內容是否相等時,都要記得使用 equals()方法,而不是使用關係運算子"==",關係運算子"=="用於兩個參考名稱比較時只有一個作用:比較兩個名稱是否參考至同一物件。

• 自動拆箱

您也可以在 JDK 5.0 中使用自動拆箱機制,實際舉個例子,在 JDK 5.0 之前,您可能會撰寫以下的語法:

Integer iRef = new Integer(10);
// ...一些陳述句
int iVar = iRef.intValue();
                            

在 JDK 5.0 之後,第一行可以改用自動裝箱,而第三行可以使用自動拆箱,也就是改用以下的語法:

Integer iRef = 10; // 自動裝箱
// ...一些陳述句
int iVar = iRef;
                            

編譯器在進行編譯時,發現到您將參考名稱指定給一個變數名稱時,會嘗試自動拆箱,以上例來說,就是嘗試取得整數10 並將之指定給 iVar 變數。

自動裝箱不只可用於值的指定,在進行運算時也可以使用自動拆箱機制,例如:

Integer iRef = 10; // 自動裝箱
System.out.println(iRef + 100);
                            

編譯器在編譯時會嘗試將 iRef 的整數值取出,然後與 100 進行相加,所以上面的程式片段會顯示 110 的執行結果,您也可以如下使用自動拆箱的功能:

Integer iRef1 = 10; // 自動裝箱
Integer iRef2 = 20;
System.out.println(iRef1 + iRef2);
                            

編譯器在編譯時會嘗試分別取得 iRef1 與 iRef2 的整數值,然後將取得的值進行相加,所以上面的程式片段會顯示 30 的執行結果。

• 裝箱與拆箱背後原理

自動裝箱與拆箱得以運行的原因,在於編譯器居中幫了忙,例如當您撰寫這樣的程式時:

Integer iRef = 10;
                            

編譯器在進行編譯時,會將您的程式進行如下的語法轉換:

Integer iRef = new Integer(10);
                            

所以自動裝箱與拆箱機制主要是編譯器的功勞,也就是俗稱的編譯器蜜糖 ( Compiler sugar )語法蜜糖 ( Syntax sugar),稱之為蜜糖常是因為該功能雖然使用方便,但隱藏了一些容易被開發人員忽略的陷阱,程式碼 8-2 與 8-3 所示範的即是其中的例子。

來看看另一個陷阱,由於自動裝箱、拆箱機制主要依賴編譯器進行語法轉換,編譯器進行編譯時期檢查時會檢查語法的正確性,對於編譯器來說,下面的程式在語法上是正確的:

Integer iRef = null;
int iVar = iRef;
                            

null 關鍵字用來表示 iRef 不參考至任何一個物件,由於在第 2 行中 iRef 被指定給一個 int 變數,所以編譯器會依自動拆箱機制作如下的轉換:

Integer iRef = null;
int iVar = iRef.intValue();
                            

編譯器通過編譯了,然而在執行時期,由於 iRef 名稱實際上並沒有參考至任何物件,所以也就不可能依程式所撰寫的,操作 intValue()方法,所以會導致執行時期錯誤,程式會出現 NullPointerException 的錯誤。

重點提示

自動裝箱、拆箱機制提供了程式開發人員撰寫程式的方便性,但是使用時還是必須了解其背後運作原理,程式開發人員在使用這個機制時,必須多多思考物件與基本資料型態之間是如何進行轉換的問題,以免程式發生不必要的錯誤。

圖8-14 了解物件與基本型態的差異