Java自動裝箱與拆箱及其陷阱
在本文中,筆者向大家介紹下Java中一個非常重要也非常有趣的特性,就是自動裝箱與拆箱,並從原始碼中解讀自動裝箱與拆箱的原理,同時這種特性也留有一個陷阱。開發者如果不注意,就會很容易跌入這個陷阱。
自動裝箱(Autoboxing)
定義
大家在平時編寫Java程式時,都常常以以下方式來定義一個Integer物件:
Integer i=100;
從上面的程式碼中,大家可以得知,i為一個Integer型別的引用,100為Java中的基礎資料型別(primitive data type)。而這種直接將一個基礎資料型別傳給其相應的封裝類(wrapper class)的做法,便是自動裝箱(Autoboxing)。
在jdk 1.5中,自動裝箱首次被引入。而在jdk 1.5之前,如果你想要定義一個value為100的Integer物件,則需要這樣做:
Integer i=new Integer (100);
原理
我們在以上程式碼“Integer i=100;”處打一個斷點,跟蹤一下。
接下來,我們可以看到,程式跳轉到了Integer類的valueOf(int i)方法中
/** * Returns a <tt>Integer</tt> instance representing the specified * <tt>int</tt> value. * If a new <tt>Integer</tt> instance is not required, this method * should generally be used in preference to the constructor * {@link #Integer(int)}, as this method is likely to yield * significantly better space and time performance by caching * frequently requested values. * * @param i an <code>int</code> value. * @return a <tt>Integer</tt> instance representing <tt>i</tt>. * @since 1.5 */ public static Integer valueOf(int i) { if(i >= -128 && i <= IntegerCache.high) return IntegerCache.cache[i + 128]; else return new Integer(i); }
換句話說,裝箱就是jdk自己幫你完成了呼叫Integer.valueOf(100)。
拆箱(Unboxing)
定義
Integer integer100=100;
int int100=integer100;
從上面的程式碼中,大家可看出integer100為一個Integer型別的引用,int100為一個int型別的原始資料型別。但是,我們可以將一個Integer型別的物件賦值給其相應原始資料型別的變數。這便是拆箱。
拆箱與裝箱是相反的操作。裝箱是將一個原始資料型別賦值給相應封裝類的變數。而拆箱則是將一個封裝類的變數賦值給相應原始資料型別的變數。裝箱、拆箱的名字也取得相當貼切。
原理
筆者相信大家也都猜到了,拆箱過程中jdk為我們做了什麼。我們還是通過實驗來證明我們的猜想吧。
在以上程式碼的第二行程式碼打上斷點,即在“int int100=integer100;”上打上斷點,跟蹤一下。
我們可以看到,程式跳轉到了Integer的intValue()方法。
/**
* Returns the value of this <code>Integer</code> as an
* <code>int</code>.
*/
public int intValue() {
return value;
}
也就是,jdk幫我們完成了對intValue()方法的呼叫。對於以上的實驗而言,便是呼叫integer100的intValue()方法,將其返回值賦給了int100。
擴展
實驗1
Integer integer400=400;
int int400=400;
System.out.println(integer400==int400);
在以上程式碼的第三行中,integer400與int400執行了==執行。而這兩個是不同型別的變數,到底是integer400拆箱了,還是int400裝箱了呢?執行結果是什麼呢?
==運算是判斷兩個物件的地址是否相等或者判斷兩個基礎資料型別的值是否相等。所以,大家很容易推測到,如果integer400拆箱了,則說明對比的是兩個基礎型別的值,那此時必然相等,執行結果為true;如果int400裝箱了,則說明對比的是兩個物件的地址是否相等,那此時地址必然不相等,執行結果為false。(至於為什麼筆者對它們賦值為400,就是後面將要講到的陷阱有關)。
我們實際的執行結果為true。所以是integer400拆箱了。對程式碼跟蹤的結果也證明這一點。
實驗2
Integer integer100=100;
int int100=100;
System.out.println(integer100.equals(int100));
在以上程式碼的第三行中,integer100的方法equals的引數為int100。我們知道equals方法的引數為Object,而不是基礎資料型別,因而在這裡必然是int100裝箱了。對程式碼跟蹤的結果也證明了這一點。
其實,如果一個方法中引數型別為原始資料型別,所傳入的引數型別為其封裝類,則會自動對其進行拆箱;相應地,如果一個方法中引數型別為封裝型別,所傳入的引數型別為其原始資料型別,則會自動對其進行裝箱。
實驗3
Integer integer100 = 100;
int int100 = 100;
Long long200 = 200l;
System.out.println(integer100 + int100);
System.out.println(long200 == (integer100 + int100));
System.out.println(long200.equals(integer100 + int100));
在第一個實驗中,我們已經得知,當一個基礎資料型別與封裝類進行==運算時,會將封裝類進行拆箱。那如果+、-、*、/呢?我們在這個實驗中,就可知道。
如果+運算,會將基礎資料型別裝箱,那麼:
- 第4行中,integer100+int100就會得到一個型別為Integer且value為200的物件o,並執行這個物件的toString()方法,並輸出”200”;
- 第5行中,integer100+int100就會得到一個型別為Integer且value為200的物件o,==運算將這個物件與long200物件進行對比,顯然,將會輸出false;
- 第6行中,integer100+int100就會得到一個型別為Integer且value為200的物件o,Long的equals方法將long200與o對比,因為兩都是不同型別的封裝類,因而輸出false;
如果+運算,會將封裝類進行拆箱,那麼:
- 第4行中,integer100+int100就會得到一個型別為int且value為200的基礎資料型別b,再將b進行裝箱得到o,執行這個物件的toString()方法,並輸出”200”;
- 第5行中,integer100+int100就會得到一個型別為int且value為200的基礎資料型別b1,==運算將long200進行拆箱得到b2,顯然b1==b2,輸出true;
- 第6行中,integer100+int100就會得到一個型別為int且value為200的基礎資料型別b,Long的equals方法將b進行裝箱,但裝箱所得到的是型別為Integer的物件o,因為o與long200為不同的型別的物件,所以輸出false;
程式執行的結果為:
200
true
false
因而,第二種推測是正確,即在+運算時,會將封裝類進行拆箱。
陷阱
陷阱1
Integer integer100=null;
int int100=integer100;
這兩行程式碼是完全合法的,完全能夠通過編譯的,但是在執行時,就會丟擲空指標異常。其中,integer100為Integer型別的物件,它當然可以指向null。但在第二行時,就會對integer100進行拆箱,也就是對一個null物件執行intValue()方法,當然會丟擲空指標異常。所以,有拆箱操作時一定要特別注意封裝類物件是否為null。
陷阱2
Integer i1=100;
Integer i2=100;
Integer i3=300;
Integer i4=300;
System.out.println(i1==i2);
System.out.println(i3==i4);
因為i1、i2、i3、i4都是Integer型別的,所以我們想,執行結果應該都是false。但是,真實的執行結果為“System.out.println(i1==i2);”為 true,但是“System.out.println(i3==i4);”為false。也就意味著,i1與i2這兩個Integer型別的引用指向了同一個物件,而i3與i4指向了不同的物件。為什麼呢?不都是呼叫Integer.valueOf(int i)方法嗎?
讓我們再看看Integer.valueOf(int i)方法。
/**
* Returns a <tt>Integer</tt> instance representing the specified
* <tt>int</tt> value.
* If a new <tt>Integer</tt> instance is not required, this method
* should generally be used in preference to the constructor
* {@link #Integer(int)}, as this method is likely to yield
* significantly better space and time performance by caching
* frequently requested values.
*
* @param i an <code>int</code> value.
* @return a <tt>Integer</tt> instance representing <tt>i</tt>.
* @since 1.5
*/
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
我們可以看到當i>=-128且i<=IntegerCache.high時,直接返回IntegerCache.cache[i + 128]。其中,IntegerCache為Integer的內部靜態類,其原碼如下:
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
我們可以清楚地看到,IntegerCache有靜態成員變數cache,為一個擁有256個元素的陣列。在IntegerCache中也對cache進行了初始化,即第i個元素是值為i-128的Integer物件。而-128至127是最常用的Integer物件,這樣的做法也在很大程度上提高了效能。也正因為如此,“Integeri1=100;Integer i2=100;”,i1與i2得到是相同的物件。
對比擴充套件中的第二個實驗,我們得知,當封裝類與基礎型別進行==執行時,封裝類會進行拆箱,拆箱結果與基礎型別對比值;而兩個封裝類進行==執行時,與其它的物件進行==執行一樣,對比兩個物件的地址,也即判斷是否兩個引用是否指向同一個物件。
本部落格中所有的博文都為筆者(Jairus Chan)原創。
如果您對本文有任何的意見與建議,請聯絡筆者(JairusChan)。