在 Java 程式設計中,常常要將基本資料型態數值包裝為Wrapper 物件,而另一方面為了進行數值計算,又常常必須從 Wrapper 物件中取得基本資料型態數值,為了程式設計人員撰寫程式時的方便,在 JDK 5.0 之後提供了自動裝箱( autoboxing )與拆箱( unboxing )的機制。
如果您要將數值 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-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-3 只不過將程式碼 8-2 的第 3 行與第 4 行修改為指定值為 200,其餘部份並沒有改變,卻造成了不同的執行結果,其實這邊涉及到關係運算子"=="用於參考名稱的比較問題。
當關係運算子"=="使用於兩個參考名稱之比較時,它比較的是兩個參考名稱是否參考至同一個物件,以程式碼 8-2 或8-3 為例,就是在比較 iRef1 與 iRef2 是否繫結至同一個物件。
自動裝箱機制對於整數值 -128 到 127 之間的值,在裝箱為物件之後,會存在記憶體之中一直被重複使用,而對於-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
執行結果如下:
重點提示
任何時候您要比較兩個物件的內容是否相等時,都要記得使用 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 的錯誤。
重點提示
自動裝箱、拆箱機制提供了程式開發人員撰寫程式的方便性,但是使用時還是必須了解其背後運作原理,程式開發人員在使用這個機制時,必須多多思考物件與基本資料型態之間是如何進行轉換的問題,以免程式發生不必要的錯誤。