J2SE 5.0專題 之 語言特性
1.1.背景 J2SE(TM)5.0正式發布至今已超過3個月的時間了,就在前不久,大概是在兩周之前,Sun又發布了更新過的JDK5.0Update1,改掉了一些第一個版本中出現的 bug 。 由于 Java 社群等待這一從1.4向5.0版本升級已經有相當長的一段時間,大家都很關心5.0中有哪些
1.1. 背景
J2SE(TM) 5.0正式發布至今已超過3個月的時間了,就在前不久,大概是在兩周之前,Sun又發布了更新過的JDK 5.0 Update 1,改掉了一些第一個版本中出現的
bug。
由于
Java社群等待這一從1.4向5.0版本升級已經有相當長的一段時間,大家都很關心5.0中有哪些值得關注的變化,于是blog的相關信息滿天飛,我也興沖沖地在自己的blog中添上了一系列的文章。無奈這些blog文章,包括我自己的在內,通常都是泛泛而談,因此CSDN第二期Java電子
雜志的編輯們計劃做一個
專題對這一話題與相關人士進行一番深入的探討。
作為這期電子刊物的一部分,編輯們也邀請我更系統的探討一下:J2SE(TM) 5.0中新引入的語言特性究竟在實際中有哪些用途,以及為什么要引入這些新特性。對此我深感榮幸。我本人很樂意將我的一些也許算得上經驗的Java經驗跟大家分享,希望這一篇小文能對大家了解J2SE(TM) 5.0有一定幫助。
1.2. 準備工作
首先,為了了解J2SE(TM) 5.0的新的語言特性,你需要
下載新版的JDK,在這里可以找到下載鏈接: http://java.sun.com/j2se/1.5.0/download.jsp 。當然,如果你已經有過手動配置Java環境的經歷,我也建議你使用一個支持J2SE(TM) 5.0的IDE,推薦Eclipse SDK 3.1 M4,或者NetBeans IDE 4.0。兩個都是
開源免費的,且很容易找到(Eclipse不用說了,NetBeans IDE 4.0有與JDK 5.0 Update 1的捆綁版)。
說點題外話,Java的版本號自從1.2開始,似乎就多少顯得有點蹩腳。從1.2版本開始,Java (J2SE)被稱作Java 2,而不是Java 1.2,現在則顯得更加離奇:Java(TM) 2 Platform Standard Edition 5.0或者J2SE(TM) 5.0,而內部的版本號還是1.5.0。那么到底是1、2、還是5呢?來看看Sun官方網站是怎么說的:
從Java誕生至今已有9年時間,而從第二代Java平臺J2SE算起也有5個年頭了。在這樣的背景下,將下一個版本的版本號從1.5改為5.0可以更好的反映出新版J2SE的成熟度、穩定性、可伸縮性和
安全性。
好吧,現在我們將面對如下一些名稱,而它們指的基本上是同一個東西:
· Tiger
· Java(TM) 2 Platform Standard Edition 5.0
· J2SE(TM) 5.0
· Java version 1.5.0
· …
在本文中,為了方便起見,我將統一使用J2SE(TM) 5.0這個名稱。
如果你對Java各個版本的代號感興趣,就像這里的"Tiger",可以參考如下網址: http://java.sun.com/j2se/codenames.html 。透露一點:Java下一個版本(6.0)的代號是"Mustang"野馬,再下一個版本(7.0)的代號是"Dolphin"海豚。
1.3. 概述
J2SE(TM) 5.0引入了很多激進的語言元素變化,這些變化或多或少減輕了我們
開發人員的一些編碼負擔,其中的大部分也必然會被應用到即將發布的J2EE(TM) 5.0中。主要的新特性包括:
· 泛型
· 增強的for循環
· 自動裝箱和自動拆箱
· 類型安全的枚舉
· 可變長度參數
· 靜態引入
· 元數據(注解)
· C風格的格式化輸出
這當中,泛型、枚舉和注解可能會占用較大的篇幅,而其余的因為用法直截了當,抑或相對簡單,我就稍作介紹,剩下的留給讀者去思考、去探索了。
1.4. 泛型
泛型這個題目相當大,大到完全可以就這個話題寫一本書。有關Java是否需要泛型和如何實現泛型的討論也早就在Java社群廣為流傳。終于,我們在J2SE(TM) 5.0中看到了它。也許目前Java對泛型的支持還算不上足夠理想,但這一特性的添加也經足以讓我們欣喜一陣了。
在接下來的介紹中,我們會了解到:Java的泛型雖然跟C++的泛型看上去十分相似,但其實有著相當大的區別,有些細節的東西也相當復雜(至少很多地方會跟我們的直覺背道而馳)??梢赃@樣說,泛型的引入在很大程度上增加了Java語言的復雜度,對初學者尤其是個挑戰。下面我們將一點一點往里挖。
首先我們來看一個簡單的使用泛型類的例子:
ArrayList<Integer> aList = new ArrayList<Integer>();
aList.add(new Integer(1));
// ...
Integer myInteger = aList.get(0);
我們可以看到,在這個簡單的例子中,我們在定義aList的時候指明了它是一個直接受Integer類型的ArrayList,當我們調用aList.get(0)時,我們已經不再需要先顯式的將結果轉換成Integer,然后再賦值給myInteger了。而這一步在早先的Java版本中是必須的。也許你在想,在使用Collection時節約一些類型轉換就是Java泛型的全部嗎?遠不止。單就這個例子而言,泛型至少還有一個更大的好處,那就是使用了泛型的容器類變得更加健壯:早先,Collection接口的get()和Iterator接口的next()方法都只能返回Object類型的結果,我們可以把這個結果強制轉換成任何Object的子類,而不會有任何編譯期的錯誤,但這顯然很可能帶來嚴重的運行期錯誤,因為在代碼中確定從某個Collection中取出的是什么類型的對象完全是調用者自己說了算,而調用者也許并不清楚放進Collection的對象具體是什么類的;就算知道放進去的對象“應該”是什么類,也不能保證放到Collection的對象就一定是那個類的實例?,F在有了泛型,只要我們定義的時候指明該Collection接受哪種類型的對象,編譯器可以幫我們避免類似的問題溜到產品中。我們在實際工作中其實已經看到了太多的ClassCastException,不是嗎?
泛型的使用從這個例子看也是相當易懂。我們在定義ArrayList時,通過類名后面的<>括號中的值指定這個ArrayList接受的對象類型。在編譯的時候,這個ArrayList會被處理成只接受該類或其子類的對象,于是任何試圖將其他類型的對象添加進來的語句都會被編譯器拒絕。
那么泛型是怎樣定義的呢?看看下面這一段示例代碼:(其中用E代替在實際中將會使用的類名,當然你也可以使用別的名稱,習慣上在這里使用大寫的E,表示Collection的元素。)
public class TestGenerics<E> {
Collection<E> col;
public void doSth(E elem) {
col.add(elem);
// ...
}
}
在泛型的使用中,有一個很容易有的誤解,那就是既然Integer是從Object派生出來的,那么ArrayList<Integer>當然就是ArrayList<Object>的子類。真的是這樣嗎?我們仔細想一想就會發現這樣做可能會帶來的問題:如果我們可以把ArrayList<Integer>向上轉型為ArrayList<Object>,那么在往這個轉了型以后的ArrayList中添加對象的時候,我們豈不是可以添加任何類型的對象(因為Object是所有對象的公共父類)?這顯然讓我們的ArrayList<Integer>失去了原本的目的。于是Java編譯器禁止我們這樣做。那既然是這樣,ArrayList<Integer>以及ArrayList<String>、ArrayList<Double>等等有沒有公共的父類呢?有,那就是ArrayList<?>。?在這里叫做通配符。我們為了縮小通配符所指代的范圍,通常也需要這樣寫:ArrayList<? extends SomeClass>,這樣寫的含義是定義這樣一個類ArrayList,比方說SomeClass有SomeExtendedClass1和SomeExtendedClass2這兩個子類,那么ArrayList<? extends SomeClass>就是如下幾個類的父類:ArrayList<SomeClass>、ArrayList<SomeExtendedClass1>和ArrayList<SomeExtendedClass2>。
接下來我們更進一步:既然ArrayList<? extends SomeClass>是一個通配的公用父類,那么我們可不可以往聲明為ArrayList<? extends SomeClass>的ArrayList實例中添加一個SomeExtendedClass1的對象呢?答案是不能。甚至你不能添加任何對象。為什么?因為ArrayList<? extends SomeClass>實際上代表了所有ArrayList<SomeClass>、ArrayList<SomeExtendedClass1>和ArrayList<SomeExtendedClass2>三種ArrayList,甚至包括未知的接受SomeClass其他子類對象的ArrayList。我們拿到一個定義為ArrayList<? extends SomeClass>的ArrayList的時候,我們并不能確定這個ArrayList具體是使用哪個類作為參數定義的,因此編譯器也無法讓這段代碼編譯通過。舉例來講,如果我們想往這個ArrayList中放一個SomeExtendedClass2的對象,我們如何保證它實際上不是其他的如ArrayList<SomeExtendedClass1>,而就是這個ArrayList<SomeExtendedClass2>呢?(還記得嗎?ArrayList<Integer>并非ArrayList<Object>的子類。)怎么辦?我們需要使用泛型方法。泛型方法的定義類似下面的例子:
public static <T extends SomeClass> void add (Collection<T> c, T elem) {
c.add(elem);
}
其中T代表了我們這個方法期待的那個最終的具體的類,相關的聲明必須放在方法簽名中緊靠返回類型的位置之前。在本例中,它可以是SomeClass或者SomeClass的任何子類,其說明<T entends SomeClass>放在void關鍵字之前(只能放在這里)。這樣我們就可以讓編譯器確信當我們試圖添加一個元素到泛型的ArrayList實例中時,可以保證類型安全。
Java泛型的最大特點在于它是在語言級別實現的,區別于C# 2.0中的C
LR級別。這樣的做法使得JRE可以不必做大的調整,缺點是無法支持一些運行時的類型甄別。一旦編譯,它就被寫死了,能提供的動態能力相當弱。
個人認為泛型是這次J2SE(TM) 5.0中引入的最重要的語言元素,給Java語言帶來的影響也是最大。舉個例子來講,我們可以看到,幾乎所有的Collections API都被更新成支持泛型的版本。這樣做帶來的好處是顯而易見的,那就是減少代碼重復(不需要提供多個版本的某一個類或者接口以支持不同類的對象)以及增強代碼的健壯性(編譯期的類型安全檢查)。不過如何才能真正利用好這個特性,尤其是如何實現自己的泛型接口或類供他人使用,就并非那么顯而易見了。讓我們一起在使用中慢慢積累。
1.5. 增強的for循環
你是否已經厭倦了每次寫for循環時都要寫上那些機械的代碼,尤其當你需要遍歷數組或者Collection,如:(假設在Collection中儲存的對象是String類型的)
public void showAll (Collection c) {
for (Iterator iter = c.iterator(); iter.hasNext(); ) {
System.out.println((String) iter.next());
}
}
public void showAll (String[] sa) {
for (int i = 0; i < sa.length; i++) {
System.out.println(sa[i]);
}
}
這樣的代碼不僅顯得臃腫,而且容易出錯,我想我們大家在剛開始接觸編程時,尤其是C/C++和Java,可能多少都犯過以下類似錯誤的一種或幾種:把for語句的三個表達式順序弄錯;第二個表達式邏輯判斷不正確(漏掉一些、多出一些、甚至死循環);忘記移動游標;在循環體內不小心改變了游標的位置等等。為什么不能讓編譯器幫我們處理這些細節呢?在5.0中,我們可以這樣寫:
public void showAll (Collection c) {
for (Object obj : c) {
System.out.println((String) obj);
}
}
public void showAll (String[] sa) {
for (String str : sa) {
System.out.println(str);
}
}
這樣的代碼顯得更加清晰和簡潔,不是嗎?具體的語法很簡單:使用":"分隔開,前面的部分寫明從數組或Collection中將要取出的類型,以及使用的臨時變量的名字,后面的部分寫上數組或者Collection的引用。加上泛型,我們甚至可以把第一個方法變得更加漂亮:
public void showAll (Collection<String> cs) {
for (String str : cs) {
System.out.println(str);
}
}
有沒有發現:當你需要將Collection<String>替換成String[],你所需要做的僅僅是簡單的把參數類型"Collection<String>"替換成"String[]",反過來也是一樣,你不完全需要改其他的東西。這在J2SE(TM) 5.0之前是無法想象的。
對于這個看上去相當方便的新語言元素,當你需要在循環體中訪問游標的時候,會顯得很別扭:比方說,當我們處理一個鏈表,需要更新其中某一個元素,或者刪除某個元素等等。這個時候,你無法在循環體內獲得你需要的游標信息,于是需要回退到原先的做法。不過,有了泛型和增強的for循環,我們在大多數情況下已經不用去操心那些煩人的for循環的表達式和嵌套了。畢竟,我們大部分時間都不會需要去了解游標的具體位置,我們只需要遍歷數組或Collection,對吧?
1.6. 自動裝箱/自動拆箱
所謂裝箱,就是把值類型用它們相對應的引用類型包起來,使它們可以具有對象的特質,如我們可以把int型包裝成Integer類的對象,或者把double包裝成Double,等等。所謂拆箱,就是跟裝箱的方向相反,將Integer及Double這樣的引用類型的對象重新簡化為值類型的數據。
在J2SE(TM) 5.0發布之前,我們只能手工的處理裝箱和拆箱。也許你會問,為什么需要裝箱和拆箱?比方說當我們試圖將一個值類型的數據添加到一個Collection中時,就需要先把它裝箱,因為Collection的add()方法只接受對象;而當我們需要在稍后將這條數據取出來,而又希望使用它對應的值類型進行操作時,我們又需要將它拆箱成值類型的版本?,F在,編譯器可以幫我們自動地完成這些必要的步驟。下面的代碼我提供兩個版本的裝箱和拆箱,一個版本使用手工的方式,另一個版本則把這些顯而易見的代碼交給編譯器去完成:
public static void manualBoxingUnboxing(int i) {
ArrayList<Integer> aList = new ArrayList<Integer>();
aList.add(0, new Integer(i));
int a = aList.get(0).intValue();
System.out.println("The value of i is " + a);
}
public static void autoBoxingUnboxing(int i) {
ArrayList<Integer> aList = new ArrayList<Integer>();
aList.add(0, i);
int a = aList.get(0);
System.out.println("The value of i is " + a);
}
看到了吧,在J2SE(TM) 5.0中,我們不再需要顯式的去將一個值類型的數據轉換成相應的對象,從而把它作為對象傳給其他方法,也不必手工的將那個代表一個數值的對象拆箱為相應的值類型數據,只要你提供的信息足夠讓編譯器確信這些裝箱/拆箱后的類型在使用時是合法的:比方講,如果在上面的代碼中,如果我們使用的不是ArrayList<Integer>而是ArrayList或者其他不兼容的版本如ArrayList<java.util.Date>,會有編譯錯誤。
當然,你需要足夠重視的是:一方面,對于值類型和引用類型,在資源的占用上有相當大的區別;另一方面,裝箱和拆箱會帶來額外的開銷。在使用這一方便特性的同時,請不要忘記了背后隱藏的這些也許會影響
性能的因素。
1.7. 類型安全的枚舉
在介紹J2SE(TM) 5.0中引入的類型安全枚舉的用法之前,我想先簡單介紹一下這一話題的背景。
我們知道,在C中,我們可以定義枚舉類型來使用別名代替一個集合中的不同元素,通常是用于描述那些可以歸為一類,而又具備有限數量的類別或者概念,如月份、顏色、撲克牌、太陽系的行星、五大洲、四大洋、季節、學科、四則運算符,等等。它們通??瓷先ナ沁@個樣子:
typedef enum {SPRING, SUMMER, AUTUMN, WINTER} season;
實質上,這些別名被處理成int常量,比如0代表SPRING,1代表SUMMER,以此類推。因為這些別名最終就是int,于是你可以對它們進行四則運算,這就造成了語意上的不明確。
原文轉自:http://www.anti-gravitydesign.com