Java 5.0發布了,許多人都將開始使用這個JDK版本的一些新增特性。從增強的for循環到諸如泛型(generic)之類更復雜的特性,都將很快出現在您所編寫的代碼中。我們剛剛完成了一個基於Java 5.0的大型任務,而本文就是要介紹我們使用這些新特性的體驗。本文不是一篇入門性的文章,而是對這些特性以及它們所產生的影響的深入介紹,同時還給出了一些在項目中更有效地使用這些特性的技巧。
在JDK 1.5的beta階段,我們為BEA的Java IDE開發了一個Java 5編譯器。因為我們實現了許多新特性,所以人們開始以新的方式利用它們;有些用法很聰明,而有些用法明顯應該被列入禁用清單。編譯器本身使用了新的語言特性,所以我們也獲得了使用這些特性維護代碼的直接體驗。本文將介紹其中的許多特性和使用它們的體驗。
我們假定您已經熟悉了這些新特性,所以不再全面介紹每個特性,而是談論一些有趣的、但很可能不太明顯的內容和用法。這些技巧出自我們的實際體驗,並大致按照語言特性進行了分類。
我們將從最簡單的特性開始,逐步過渡到高級特性。泛型所包含的內容特別豐富,因此占了本文一半的篇幅。
增強的for循環
為了迭代集合和數組,增強的for循環提供了一個簡單、兼容的語法。有兩點值得一提:
Init表達式
在循環中,初始化表達式只計算一次。這意味著您通常可以移除一個變量聲明。在這個例子中,我們必須創建一個整型數組來保存computeNumbers()的結果,以防止每一次循環都重新計算該方法。您可以看到,下面的代碼要比上面的代碼整潔一些,並且沒有洩露變量numbers:
未增強的For:
int sum = 0;
Integer[] numbers = computeNumbers();
for (int i=0; i < numbers.length ; i++)
sum += numbers[i];
增強後的For:
int sum = 0;
for ( int number: computeNumbers() )
sum += number;
局限性
有時需要在迭代期間訪問迭代器或下標,看起來增強的for循環應該允許該操作,但事實上不是這樣,請看下面的例子:
for (int i=0; i < numbers.length ; i++) {
if (i != 0) System.out.print(",");
System.out.print(numbers[i]);
}
我們希望將數組中的值打印為一個用逗號分隔的清單。我們需要知道目前是否是第一項,以便確定是否應該打印逗號。使用增強的for循環是無法獲知這種信息的。我們需要自己保留一個下標或一個布爾值來指示是否經過了第一項。 這是另一個例子:
for (Iterator it = n.iterator() ; it.hasNext() ; )
if (it.next() 1) {
Exception e = (Exception)objects[1];
// Do something with the exception
}
}
方法簽名應該如下所示,相應的可變參數分別使用String和Exception聲明:
Log.log(String message, Exception e, Object... objects) {...}
不要使用可變參數破壞類型系統。需要強類型化時才可以使用它。對於這個規則,PrintStream.printf()是一個有趣的例外:它提供類型信息作為自己的第一個參數,以便稍後可以接受那些類型。
協變返回
協變返回的基本用法是用於在已知一個實現的返回類型比API更具體的時候避免進行類型強制轉換。在下面這個例子中,有一個返回Animal對象的Zoo接口。我們的實現返回一個AnimalImpl對象,但是在JDK 1.5之前,要返回一個Animal對象就必須聲明。:
public interface Zoo {
public Animal getAnimal();
}
public class ZooImpl implements Zoo {
public Animal getAnimal(){
return new AnimalImpl();
}
}
協變返回的使用替換了三個反模式:
直接字段訪問。為了規避API限制,一些實現把子類直接暴露為字段:
ZooImpl._animal
另一種形式是,在知道實現的實際上是特定的子類的情況下,在調用程序中執行向下轉換:
((AnimalImpl)ZooImpl.getAnimal()).implMethod();
我看到的最後一種形式是一個具體的方法,該方法用來避免由一個完全不同的簽名所引發的問題:
ZooImpl._getAnimal();
這三種模式都有它們的問題和局限性。要麼是不夠整潔,要麼就是暴露了不必要的實現細節。
協變
協變返回模式就比較整潔、安全並且易於維護,它也不需要類型強制轉換或特定的方法或字段:
public AnimalImpl getAnimal(){
return new AnimalImpl();
}
使用結果:
ZooImpl.getAnimal().implMethod();
使用泛型
我們將從兩個角度來了解泛型:使用泛型和構造泛型。我們不討論List、Set和Map的顯而易見的用法。知道泛型集合是強大的並且應該經常使用就足夠了。
我們將討論泛型方法的使用以及編譯器推斷類型的方法。通常這些都不會出問題,但是當出問題時,錯誤信息會非常令人費解,所以需要了解如何修復這些問題。
泛型方法
除了泛型類型,Java 5還引入了泛型方法。在這個來自Java.util.Collections的例子中,構造了一個單元素列表。新的List的元素類型是根據傳入方法的對象的類型來推斷的:
static List Collections.singletonList(T o)
示例用法:
public List getListOfOne() {
return Collections.singletonList(1);
}
示例用法:
在示例用法中,我們傳入了一個int。所以方法的返回類型就是List。編譯器把T推斷為Integer。這和泛型類型是不同的,因為您通常不需要顯式地指定類型參數。
這也顯示了自動裝箱和泛型的相互作用。類型參數必須是引用類型:這就是為什麼我們得到的是List而不是List。
不帶參數的泛型方法
emptyList()方法與泛型一起引入,作為Java.util.Collections中EMPTY_LIST字段的類型安全置換:
static List Collections.emptyList()
示例用法:
public List getNoIntegers() {
return Collections.emptyList();
}
與先前的例子不同,這個方法沒有參數,那麼編譯器如何推斷T的類型呢?基本上,它將嘗試使用一次參數。如果沒有起作用,它再次嘗試使用返回或賦值類型。在本例中,返回的是List,所以T被推斷為Integer。
如果在返回語句或賦值語句之外的位置調用泛型方法會怎麼樣呢?那麼編譯器將無法執行類型推斷的第二次傳送。在下面這個例子中,emptyList()是從條件運算符內部調用的:
public List getNoIntegers() {
return x ? Collections.emptyList() : null;
}
因為編譯器看不到返回上下文,也不能推斷T,所以它放棄並采用Object。您將看到一個錯誤消息,比如:“無法將List轉換為List。” 為了修復這個錯誤,應顯式地向方法調用傳遞類型參數。這樣,編譯器就不會試圖推斷類型參數,就可以獲得正確的結果:
return x ? Collections.emptyList() : null;
這種情況經常發生的另一個地方是在方法調用中。如果一個方法帶一個List參數,並且需要為那個參數調用這個傳遞的emptyList(),那麼也需要使用這個語法。
集合之外
這裡有三個泛型類型的例子,它們不是集合,而是以一種新穎的方式使用泛型。這三個例子都來自標准的Java庫:
" Class
Class在類的類型上被參數化了。這就使無需類型強制轉換而構造一個newInstance成為可能。
" Comparable
Comparable被實際的比較類型參數化。這就在compareTo()調用時提供了更強的類型化。例如,String實現Comparable。對除String之外的任何東西調用compareTo(),都會在編譯時失敗。
" Enum
Enum被枚舉類型參數化。一個名為Color的枚舉類型將擴展Enum。getDeclaringClass()方法返回枚舉類型的類對象,在這個例子中就是一個Color對象。它與getClass()不同,後者可能返回一個無名類。
通配符
泛型最復雜的部分是對通配符的理解。我們將討論三種類型的通配符以及它們的用途。
首先讓我們了解一下數組是如何工作的。可以從一個Integer[]為一個Number[]賦值。如果嘗試把一個Float寫到Number[]中,那麼可以編譯,但在運行時會失敗,出現一個ArrayStoreException:
Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5; // compiles, but fails at runtime
如果試圖把該例直接轉換成泛型,那麼會在編譯時失敗,因為賦值是不被允許的:
List iList = new ArrayList();
List nList = iList; // not allowed
nList.add(0.5);
如果使用泛型,只要代碼在編譯時沒有出現警告,就不會遇到運行時ClassCastException。
上限通配符
我們想要的是一個確切元素類型未知的列表,這一點與數組是不同的。
List是一個列表,其元素類型是具體類型Number。
List是一個確切元素類型未知的列表。它是Number或其子類型。
上限
如果我們更新初始的例子,並賦值給List,那麼現在賦值就會成功了:
List iList = new ArrayList();
List nList = iList;
Number n = nList.get(0);
nList.add(0.5); // Not allowed
我們可以從列表中得到Number,因為無論列表的確切元素類型是什麼(Float、Integer或Number),我們都可以把它賦值給Number。
我們仍然不能把浮點類型插入列表中。這會在編譯時失敗,因為我們不能證明這是安全的。如果我們想要向列表中添加浮點類型,它將破壞iList的初始類型安全——它只存儲Integer。
通配符給了我們比數組更多的表達能力。
為什麼使用通配符
在下面這個例子中,通配符用於向API的用戶隱藏類型信息。在內部,Set被存儲為CustomerImpl。而API的用戶只知道他們正在獲取一個Set,從中可以讀取Customer。
此處通配符是必需的,因為無法從Set向Set賦值:
public class CustomerFactory {
private Set _customers;
public Set getCustomers() {
return _customers;
}
}
通配符和協變返回
通配符的另一種常見用法是和協變返回一起使用。與賦值相同的規則可以應用到協變返回上。如果希望在重寫的方法中返回一個更具體的泛型類型,聲明的方法必須使用通配符:
public interface NumberGenerator {
public List generate();
}
public class FibonacciGenerator extends NumberGenerator {
public List generate() {
...
}
}
如果要使用數組,接口可以返回Number[],而實現可以返回Integer[]。
下限
我們所談的主要是關於上限通配符的。還有一個下限通配符。List是一個確切“元素類型”未知的列表,但是可能是Mnumber,或者Number的超類型。所以它可能是一個List或一個List。
下限通配符遠沒有上限通配符那樣常見,但是當需要它們的時候,它們就是必需的。
下限與上限
List readList = new ArrayList();
Number n = readList.get(0);
List writeList = new ArrayList();
writeList.add(new Integer(5));
第一個是可以從中讀數的列表。
第二個是可以向其寫數的列表。
無界通配符
最後,List列表的內容可以是任何類型,而且它與List幾乎相同。可以隨時讀取Object,但是不能向列表中寫入內容。
公共API中的通配符
總之,正如前面所說,通配符在向調用程序隱藏實現細節方面是非常重要的,但即使下限通配符看起來是提供只讀訪問,由於remove(int position)之類的非泛型方法,它們也並非如此。如果您想要一個真正不變的集合,可以使用Java.util.Collection上的方法,比如unmodifiableList()。
編寫API的時候要記得通配符。通常,在傳遞泛型類型時,應該嘗試使用通配符。它使更多的調用程序可以訪問API。
通過接收List而不是List,下面的方法可以由許多不同類型的列表調用:
void removeNegatives(List list);
構造泛型類型
現在我們將討論構造自己的泛型類型。我們將展示一些例子,其中通過使用泛型可以提高類型安全性,我們還將討論一些實現泛型類型時的常見問題。
集合風格(Collection-like)的函數
第一個泛型類的例子是一個集合風格的例子。Pair有兩個類型參數,而且字段是類型的實例:
public final class Pair {
public final A first;
public final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
}
這使從方法返回兩個項而無需為每個兩種類型的組合編寫專用的類成為可能。另一種方法是返回Object[],而這樣是類型不安全或者不整潔的。
在下面的用法中,我們從方法返回一個File和一個Boolean。方法的客戶端可以直接使用字段而無需類型強制轉換:
public Pair getFileAndWriteStatus(String path){
// create file and status
return new Pair(file, status);
}
Pair result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;
集合之外
在下面這個例子中,泛型被用於附加的編譯時安全性。通過把DBFactory類參數化為所創建的Peer類型,您實際上是在強制Factory子類返回一個Peer的特定子類型:
public abstract class DBFactory {
protected abstract T createEmptyPeer();
public List get(String constraint) {
List peers = new ArrayList();
// database magic
return peers;
}
}
通過實現DBFactory,CustomerFactory必須從createEmptyPeer()返回一個Customer:
public class CustomerFactory extends DBFactory{
public Customer createEmptyPeer() {
return new Customer();
}
}
泛型方法
不管想要對參數之間還是參數與返回類型之間的泛型類型施加約束,都可以使用泛型方法:
例如,如果編寫的反轉函數是在位置上反轉,那麼可能不需要泛型方法。然而,如果希望反轉返回一個新的List,那麼可能會希望新List的元素類型與傳入的List的類型相同。在這種情況下,就需要一個泛型方法:
List reverse(List list)
具體化
當實現一個泛型類時,您可能想要構造一個數組T[]。因為泛型是通過擦除(erasure)實現的,所以這是不允許的。
您可以嘗試把Object[]強制轉換為T[]。但這是不安全的。
具體化解決方案
按照泛型教程的慣例,解決方案使用的是“類型令牌”,通過向構造函數添加一個Class參數,可以強制客戶端為類的類型參數提供正確的類對象:
public class ArrayExample {
private Class clazz;
public ArrayExample(Class clazz) {
this.clazz = clazz;
}
public T[] getArray(int size) {
return (T[])Array.newInstance(clazz, size);
}
}
為了構造ArrayExample,客戶端必須把String.class傳遞給構造函數,因為String.class的類型是Class。
擁有類對象使構造一個具有正確元素類型的數組成為可能。
結束語
總而言之,新的語言特性有助於從根本上改變Java。通過了解在什麼場景下使用以及如何使用這些新特性,您將會編寫出更好的代碼。