J2SE 1.5提供了“Autoboxing”和“Auto-Unboxing”的機制,可以讓編譯器來自動完成在基本類型和它們的包裹對象之間的轉化工作,從而能夠用一種更簡單的方式,來避免同時存在兩套類型系統所帶來的一些麻煩。本文介紹Autoboxing/Auto-Unboxing機制的使用方法、實質、發生時機、局限、對重載機制的影響以及對性能的妨礙等問題。
傳統上,在Java程序中,可以往一個容器類(無論是Collection還是Map)裡直接放入一個對象;但是如果打算放入的是一個數字、字符或布爾值的話,就要先加入一個“生成包裹它們的對象”的步驟。
造成這種現象的原因是,在Java語言當中一直存在著兩套非常不同的類型系統:
一套是所謂的“引用類型”(Reference Types),包括所有的類和接口。這些類型的數據被看作對象,所以可以用一個Object型的變量來保存。
一套是所謂的“基本類型”(Primitive Types),包括:byte、short、int、long、float、double、char和boolean。這些類型的數據不是對象,因此也不能用Object型的變量來保存。
同時采用這樣兩套類型系統,可以得到一些性能方面的好處——因為基本類型的數據不是對象,所以創建得更快、占用的空間更少、收回它們占用的資源也更容易;但是,這樣的做法同時也會造成一些編碼方面的問題——例如,不能定義一個變量(或數組),讓它既能保存基本類型的數據,又能保存引用類型的數據(類似的,也不能定義一個同時能匹配這兩種類型的數據的形參,不過這個問題可以借助Java裡的重載機制來回避)。
實際上需要定義“不知道用來保存什麼類型的數據”的變量(和形參)時,一般對這個問題采取回避的態度,將它們的類型定義成Object,然後借助可以稱為“Boxing”和“Unboxing”的操作來解決Object不能涵蓋基本類型的問題。
1. Boxing和Unboxing操作
所謂Boxing操作,是指通過生成一個能包裹基本類型數據的對象,來讓基本類型的數據出現在只能接受引用類型的地方。
清單1:手工Boxing的典型情況
Collection integers = new ArrayList();
for(int i = 0; i < 10; i++) {
integers.add(new Integer(i));
}
用於生成這些的對象的類,被稱作“包裹類”(Wrapper Classes)。Java中的包裹類有Byte 、Short、Integer、Long、Float、Double、Character和Boolean(都在java.lang包裡定義)等八種,分別用於包裹byte、short、int、long、float、double、char和boolean類型的數據。
而所謂Unboxing操作,則是指調用包裹類對象的相應方法,得到它們所代表的“基本類型的數據”,以便進行進一步的處置。
清單2:手工Unboxing的典型情況
for(Iterator itr = integers.iterator(); itr.hasNext(); ) {
Integer i = (Integer) itr.next();
System.out.println(i.intValue() + 1);
}
而在Java語言的最新版本——J2SE 1.5中,提供了“Autoboxing”和“Auto-Unboxing”的機制,可以讓編譯器來自動完成這些瑣碎的操作,從而用一種更簡單的方式,來整合兩套類型系統。
熟悉的陌生名詞
盡管這一對操作的歷史很悠久,但是把它們稱作“Boxing”和“Unboxing”的做法,基本是在出現“Autoboxing”和“Auto-Unboxing”的概念之後,才得到了廣泛的接受。在那之前,它們似乎並沒有通用的、專門的名字。不過由於那時也很少提及這兩個概念,所以這個問題倒也沒有造成什麼嚴重的影響。
2. 使用Autoboxing和Auto-Unboxing
使用Autoboxing和Auto-Unboxing,並不需要什麼特別的步驟,一切都會在編譯器的安排下自動發生。
現在可以這樣來對待一個int型的數據:
清單3:自動完成的Boxing操作
Collection al = new ArrayList();
al.add(1);
因為編譯器會悄悄的把這段代碼轉換成接近這個樣子:
清單4:作了Autoboxing之後的等價形式
Collection al = new ArrayList();
al.add(Integer.valueOf(1));
這裡所用的能接受int類型的值為參數,生成Integer實例的valueOf方法,是J2SE 1.5中新加入的內容。其它包裹類也都有可以接受對應的基本類型的值為參數,生成對應的包裹類實例的valueOf方法加入。
而這樣對待一個Integer型的對象也是可以的:
清單5:自動完成的Unboxing操作
Integer one = new Integer(1);
int two = one + 1;
因為編譯器會悄悄的把這段代碼轉換成類似這個形狀:
清單6:作了Auto-Unboxing之後的等價形式
Integer one = new Integer(1);
int two = one.intValue() + 1;
大體上,只要把一個結果類型是基本類型的表達式,放到需要讓它們的包裹類出現的位置上,就會誘發Autoboxing;類似的,只要把一個結果類型是包裹類的表達式,放到只允許相應的基本類型出現的位置上,就會誘發Auto-Unboxing。
“Autoboxing/Auto-Unboxing”特性的來源
J2SE 1.5中增加的許多語言特性都可以在C#裡找到對應的東西。不過根據Bruce Eckel對Joshua Bloch的采訪,盡管Java的研發小組確實很關注C#(Joshua Bloch本人的案頭就放著一本關於C#的書),但是只有“Autoboxing/Auto-Unboxing”和“Metadata”確實是從C#中直接借鑒來的特性。
3. 發生Autoboxing的具體時機
發生Autoboxing的具體時機,主要有這麼三種:
把基本類型的數據賦給引用類型的變量時。例如把一個int型的數據賦給一個Integer型變量。
清單7:賦給引用類型的變量基本類型的數據
Integer i = 31415;
把基本類型的數據傳給引用類型的參數時。例如給一個定義成Object的參數傳遞一個boolean型的數據。
清單8:傳給引用類型的參數基本類型的數據
HashMap map = new HashMap();
map.put(true, null);
把基本類型的數據往引用類型上強制轉化時。例如在一個long型的數據前面加上(Long)。
清單9:從基本類型的數據到引用類型上強制轉化
System.out.println((Long) 27828L);
4. Autoboxing的局限
Autoboxing的機制有一個局限——只能把基本類型的數據往它們自己的包裹類(以及包裹類的上級類)上轉化。
類似這樣的代碼是不能工作的,盡管int型的數據完全可以用一個Long對象來表示:
清單10:不能同時進行自動向上轉型和Autoboxing
int i = 27828;
System.out.println((Long) i);/* 編譯時出錯 */
這是因為這段代碼實際上相當於:
清單11:Autoboxing操作會在自動向上轉型之前發生
int i = 27828;
System.out.println((Long) Integer.valueOf(i));/* 編譯時出錯 */
而Integer並不是Long的子類,所以這個轉化無法進行。如果一定要進行這種操作,需要手工追加一次轉型:
清單12:需要先強制向上轉型,再作Boxing
int i = 27828;
System.out.println((Long)(long) i);
5. 發生Auto-Unboxing的具體時機
發生Auto-Unboxing的具體時機,則主要有這麼七種:
把包裹類對象賦給基本類型的變量時。例如把一個Integer型的數據賦給一個int型變量。
清單13:賦給基本類型的變量包裹類對象
int i = new Integer(32);
把包裹類對象傳給基本類型的參數時。---www.bianceng.cn。例如給一個定義成boolean的參數傳遞一個Boolean型的數據。
清單14:傳給基本類型的參數包裹類對象
JFrame frame = new JFrame("^_^");
frame.setSize(320, 200);
frame.setVisible(new Boolean(true));
把包裹類對象往基本類型上強制轉化時。例如在一個Long型的數據前面加上(long)。
清單15:從包裹類對象到基本類型的強制轉化
Long l = new Long(31415L);
System.out.println((long) l);
把包裹類對象當作運算符的操作數時。例如在兩個Byte型的數據之間放上“+”號。
清單16:把包裹類對象當作運算符的操作數
Byte a = new Byte((byte) 1);
Byte b = new Byte((byte) -1);
System.out.println(((a++) << 2) + (~b));/* 輸出“4” */
System.out.println(a);/* 輸出“2” */
用包裹類對象來指定數組的大小時。當然,從語義上說,這個對象的類型必須是Byte、Short、Integer或Character。
清單17:用包裹類對象來指定數組的大小
Character size = new Character('★');/* Unicode: 9733 */
int[] integers = new int[size];/* 生成一個可放9733個int元素的數組 */
把包裹類對象在switch語句裡使用時。當然,從語義上說,這個對象的類型必須是Byte、Short、Integer或Character。
清單18:在switch語句裡使用包裹類對象
Character c = new Character('a');
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
System.out.println("A Vowel in English");
break;
default:
System.out.println("Not A Vowel in English");
break;
}
把Boolean對象在if/for/while/do-while語句中作為條件表達式使用時。
清單19:把Boolean對象作為條件表達式
Boolean bool = new Boolean(Math.random() > 0.5);
if (bool) {
System.out.println("Aye!");
} else {
System.out.println("Nay!");
}
6. Auto-Unboxing的局限
Auto-Unboxing的機制則有這樣一個局限——只能把包裹類對象往它們對應的基本類型(以及容納范圍更廣的類型)上轉化。
類似這樣的代碼是不能工作的,盡管32並未超出byte所能表示的范圍:
清單20:不能同時進行Auto-Unboxing和強制向下轉型
Integer i = new Integer(32);
System.out.println((byte) i);/* 編譯時出錯 */
這是因為編譯器並不認可同時進行Auto-Unboxing和強制向下轉型的操作,所以這個轉化無法進行。如果一定要進行這種操作,需要手工補充一次轉型:
清單21:需要先作Unboxing,再強制向下轉型
Integer i = new Integer(32);
System.out.println((byte)(int) i);
不過同時進行Auto-Unboxing和強制向上轉型的操作是沒有問題的,所以下面的代碼工作得很正常:
清單22:可以同時進行Auto-Unboxing和強制向上轉型
Integer i = new Integer(32);
System.out.println((double) i);
7. 其它不能自動轉化的情況
除去強制類型轉化時的限制之外,還有這樣一些情況下不會發生Autoboxing/Auto-Unboxing:
1. 基本類型的數組和包裹類數組之間不會自動轉化。這樣的代碼完全不被編譯器接受:
清單23:元素可以,容器不行
int[] ints = {1, 2, 3};
Integer[] integers = ints;/* 編譯時出錯 */
2. 不能對著基本類型的表達式來調用包裹類裡的方法。這樣的申請會被編譯器徹底拒絕:
清單24:沒有方法,就是沒有方法
int i = 1;
byte b = i.byteValue();/* 編譯時出錯 */
8. null的轉化問題
Java裡的引用類型可以有一個特別的取值——“null”。試圖對null進行Auto-Unboxing操作會導致一個“NullPointerException”。
例如這段代碼就會在運行時拋出異常,盡管在編譯期間會表現得非常正常:
清單25:表面上,只是普通的賦值
Integer i = null;
int j = i;/* 運行時錯誤 */
這是因為這段代碼實際上相當於:
清單26:實際上,是在試圖調用null的方法
Integer i = null;
int j = i.intValue();/* 運行時錯誤 */
而試圖調用null的方法是一種不被虛擬機認可的行為。
如果沒記住有關的規則
大部分違反了使用“Autoboxing/Auto-Unboxing”機制時,需要遵守的約束的代碼,都會造成編譯錯誤。因此,即使並未准確的記住有關的規則,也不難及時發現、改正。只有違背了“不能對null進行Auto-Unboxing操作”的限制時,引發的是運行時異常,需要特別小心。
9. 對重載的影響
Java支持“重載”的機制,允許在同一個類擁有許多名稱相同而形參列表不同的方法。然後,由編譯器根據調用時的實參來選擇到底要執行哪一個。
Autoboxing/Auto-Unboxing機制的引入,稍微增加了一些作這種選擇時要考慮的因素——因為可能會有一個方法,既有一個能接受一個Integer型參數的版本,又有一個能接受一個int型參數的版本,而Autoboxing/Auto-Unboxing機制能自動的把實參在這兩種類型之間轉化,光憑原有的判斷規則,二者是難以取捨的。但是,因為同時有這兩個版本的做法完全合情合理,又不能在這裡給出一個“reference to 被調用的方法名 is ambiguous”的編譯錯誤來推卸責任。這就需要增加一條新的判斷規則。
這條新增的規則是,不用進行Autoboxing/Auto-Unboxing的版本,優先於需要進行Autoboxing/Auto-Unboxing的版本。
因此,在這種情況下具體選擇哪一個,要看傳遞的實參最初是什麼類型。
清單27:不用進行Autoboxing/Auto-Unboxing的版本優先
public class OverloadingTest
{
private static void testOverloading(int i){
System.out.println("int");
}
private static void testOverloading(Integer i){
System.out.println("Integer");
}
public static void main(String[] args)
{
int i = 1;
Integer j = new Integer(1);
testOverloading(i);/* 輸出“int” */
testOverloading(j);/* 輸出“Integer” */
}
}
10. 值相等和引用相等
在Java語言中有兩個不同的“相等”概念——值相等和引用相等。這樣就有一個“兩個值相等的基本類型數據,經過Autoboxing之後,得到的對象的引用是否相等”的問題。
在《JSR 201: Extending the Java Programming Language with Enumerations, Autoboxing, Enhanced for loops and Static Import》中,對這個問題,是作了這樣的規定:
If the value p being boxed is true, false, a byte, an ASCII character, or an integer or short number between -127 and 128, then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.
這意味著這個答案可能是“是”也可能是“否”,由被Autoboxing的數據的類型和取值來決定。因此在檢測兩個對象是否代表相同的值的時候,還是有必要調用equals()方法來進行。
不過在J2SDK 1.5 Beta 1和Beta 2裡的實際情況,和這稍微有些出入,“Autoboxing之後得到相同的對象引用”的范圍被縮小了:
清單28:原來的值相等,經過Autoboxing之後的引用可能相等,也可能不相等
boolean b = true;
Boolean b1 = b;
Boolean b2 = b;
System.out.println(b1 == b2);/* 輸出“true” */
char c = '1';
Character c1 = c;
Character c2 = c;
System.out.println(c1 == c2);/* 輸出“false” */
11. 對性能的妨礙
由於Autoboxing機制的實質是“自動創建能代表基本類型數據的對象”,所以,不可避免的會對性能造成一些妨礙。
如果只是利用Autoboxing/Auto-Unboxing機制來保存基本類型的數據(例如把基本類型的數據放到Collection裡面之類),這種影響倒還可以忽略,因為這只是把原來需要手工進行的工作自動化了;但是,如果要頻繁的借助Autoboxing來給一個包裹類變量賦值,這開銷很容易上升到需要加以注意的程度。
注意對包裹類的變量使用“++”和“--”運算符的時候,也會創建新的對象,而不是在修改原來對象的狀態。
清單29:是替換不是修改
Integer i = new Integer(1);
Integer j = i;/* 讓j、i指向同一對象 */
System.out.println(j == i);/* 目前j、i是同一對象,因此輸出“true” */
i++;
System.out.println(j == i);/* 現在j、i是不同對象,因此輸出“false” */
System.out.println(i);/* 現在i的值是“2” */
System.out.println(j);/* 而j的值仍是“1” */
這個現象是由於Java裡的包裹類是“不可變的(immutable)”——即沒有提供一種能讓自己所代表的值發生變化的途徑——而造成的。
在需要大量賦值操作的時候,可以通過適當使用一些基本類型的局部變量來減輕對性能方面的影響。不過,如果性能的瓶頸在於要往一個容器裡頻繁放入基本類型的數據的話,恐怕就得靠改用一些專門為容納基本類型的數據而設計的容器類來解決了(例如Jarkata Commons Primitives組件裡提供的那些)。
清單30:一段需要往一個容器裡頻繁放入基本類型的數據的程序
import java.util.*;
public class WordCounter {
public static void main(String[] args) {
HashMap counts = new HashMap();
for (int i = 0; i < args.length; i++) {
String current = args[i];
if (counts.containsKey(current)) {
counts.put(current, ((Integer) counts.get(current)) + 1);
} else {
counts.put(current, 1);
}
}
for (Iterator itr = counts.keySet().iterator(); itr.hasNext();) {
String key = (String) itr.next();
System.out.println(key + ":" + counts.get(key));
}
}
}
12. 歸納總結
借助J2SE 1.5裡提供的Autoboxing/Auto-Unboxing機制,可以用一種更簡單的方式,來解決同時存在兩套類型系統而造成的一些不方便。不過,這種機制並沒有解決所有的相關問題,有些工作還是需要靠手工操作來進行。另外,由於不恰當的使用這一機制會造成一些性能方面的負面影響,所以在使用的時候還要注意一些問題才行。