1. 什麼是泛型?
泛型(Generic type 或者 generics)是對 Java 語言的類型系統的一種擴展,以支持創建可以按類型進行參數化的類。可以把類型參數看作是使用參數化類型時指定的類型的一個占位符,就像方法的形式參數是運行時傳遞的值的占位符一樣。
可以在集合框架(Collection framework)中看到泛型的動機。例如,Map 類允許您向一個 Map 添加任意類的對象,即使最常見的情況是在給定映射(map)中保存某個特定類型(比如 String)的對象。
因為 Map.get() 被定義為返回 Object,所以一般必須將 Map.get() 的結果強制類型轉換為期望的類型,如下面的代碼所示:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
要讓程序通過編譯,必須將 get() 的結果強制類型轉換為 String,並且希望結果真的是一個 String。但是有可能某人已經在該映射中保存了不是 String 的東西,這樣的話,上面的代碼將會拋出 ClassCastException。
理想情況下,您可能會得出這樣一個觀點,即 m 是一個 Map,它將 String 鍵映射到 String 值。這可以讓您消除代碼中的強制類型轉換,同時獲得一個附加的類型檢查層,該檢查層可以防止有人將錯誤類型的鍵或值保存在集合中。這就是泛型所做的工作。
2. 泛型的好處
Java 語言中引入泛型是一個較大的功能增強。不僅語言、類型系統和編譯器有了較大的變化,以支持泛型,而且類庫也進行了大翻修,所以許多重要的類,比如集合框架,都已經成為泛型化的了。這帶來了很多好處:
類型安全。 泛型的主要目標是提高 Java 程序的類型安全。通過知道使用泛型定義的變量的類型限制,編譯器可以在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就只存在於程序員的頭腦中(或者如果幸運的話,還存在於代碼注釋中)。
Java 程序中的一種流行技術是定義這樣的集合,即它的元素或鍵是公共類型的,比如“String 列表”或者“String 到 String 的映射”。通過在變量聲明中捕獲這一附加的類型信息,泛型允許編譯器實施這些附加的類型約束。類型錯誤現在就可以在編譯時被捕獲了,而不是在運行時當作 ClassCastException 展示出來。將類型檢查從運行時挪到編譯時有助於您更容易找到錯誤,並可提高程序的可靠性。
消除強制類型轉換。 泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,並且減少了出錯機會。
盡管減少強制類型轉換可以降低使用泛型類的代碼的羅嗦程度,但是聲明泛型變量會帶來相應的羅嗦。比較下面兩個代碼例子。
該代碼不使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
該代碼使用泛型:
List<Integer> li = new ArrayList<Integer>();
li.put(new Integer(3));
Integer i = li.get(0);
在簡單的程序中使用一次泛型變量不會降低羅嗦程度。但是對於多次使用泛型變量的大型程序來說,則可以累積起來降低羅嗦程度。
潛在的性能收益。 泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制類型轉換(沒有泛型的話,程序員會指定這些強制類型轉換)插入生成的字節碼中。但是更多類型信息可用於編譯器這一事實,為未來版本的 JVM 的優化帶來可能。
由於泛型的實現方式,支持泛型(幾乎)不需要 JVM 或類文件更改。所有工作都在編譯器中完成,編譯器生成類似於沒有泛型(和強制類型轉換)時所寫的代碼,只是更能確保類型安全而已。
3. 泛型用法的例子
泛型的許多最佳例子都來自集合框架,因為泛型讓您在保存在集合中的元素上指定類型約束。考慮這個使用 Map 類的例子,其中涉及一定程度的優化,即 Map.get() 返回的結果將確實是一個 String:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
如果有人已經在映射中放置了不是 String 的其他東西,上面的代碼將會拋出 ClassCastException。泛型允許您表達這樣的類型約束,即 m 是一個將 String 鍵映射到 String 值的 Map。這可以消除代碼中的強制類型轉換,同時獲得一個附加的類型檢查層,這個檢查層可以防止有人將錯誤類型的鍵或值保存在集合中。
下面的代碼示例展示了 JDK 5.0 中集合框架中的 Map 接口的定義的一部分:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
注意該接口的兩個附加物:
類型參數 K 和 V 在類級別的規格說明,表示在聲明一個 Map 類型的變量時指定的類型的占位符。
在 get()、put() 和其他方法的方法簽名中使用的 K 和 V。
為了贏得使用泛型的好處,必須在定義或實例化 Map 類型的變量時為 K 和 V 提供具體的值。以一種相對直觀的方式做這件事:
Map<String, String> m = new HashMap<String, String>();
m.put("key", "blarg");
String s = m.get("key");
當使用 Map 的泛型化版本時,您不再需要將 Map.get() 的結果強制類型轉換為 String,因為編譯器知道 get() 將返回一個 String。
在使用泛型的版本中並沒有減少鍵盤錄入;實際上,比使用強制類型轉換的版本需要做更多鍵入。使用泛型只是帶來了附加的類型安全。因為編譯器知道關於您將放進 Map 中的鍵和值的類型的更多信息,所以類型檢查從執行時挪到了編譯時,這會提高可靠性並加快開發速度。
向後兼容
在 Java 語言中引入泛型的一個重要目標就是維護向後兼容。盡管 JDK 5.0 的標准類庫中的許多類,比如集合框架,都已經泛型化了,但是使用集合類(比如 HashMap 和 ArrayList)的現有代碼將繼續不加修改地在 JDK 5.0 中工作。當然,沒有利用泛型的現有代碼將不會贏得泛型的類型安全好處。
4. 泛型基礎
4.1 類型參數
在定義泛型類或聲明泛型類的變量時,使用尖括號來指定形式類型參數。形式類型參數與實際類型參數之間的關系類似於形式方法參數與實際方法參數之間的關系,只是類型參數表示類型,而不是表示值。
泛型類中的類型參數幾乎可以用於任何可以使用類名的地方。例如,下面是 java.util.Map 接口的定義的摘錄:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
Map 接口是由兩個類型參數化的,這兩個類型是鍵類型 K 和值類型 V。(不使用泛型)將會接受或返回 Object 的方法現在在它們的方法簽名中使用 K 或 V,指示附加的類型約束位於 Map 的規格說明之下。
當聲明或者實例化一個泛型的對象時,必須指定類型參數的值:
Map<String, String> map = new HashMap<String, String>();
注意,在本例中,必須指定兩次類型參數。一次是在聲明變量 map 的類型時,另一次是在選擇 HashMap 類的參數化以便可以實例化正確類型的一個實例時。
編譯器在遇到一個 Map<String, String> 類型的變量時,知道 K 和 V 現在被綁定為 String,因此它知道在這樣的變量上調用 Map.get() 將會得到 String 類型。
除了異常類型、枚舉或匿名內部類以外,任何類都可以具有類型參數。
4.2 命名類型參數
推薦的命名約定是使用大寫的單個字母名稱作為類型參數。這與 C++ 約定有所不同(參閱 附錄 A:與 C++ 模板的比較),並反映了大多數泛型類將具有少量類型參數的假定。對於常見的泛型模式,推薦的名稱是:
K —— 鍵,比如映射的鍵。
V —— 值,比如 List 和 Set 的內容,或者 Map 中的值。
E —— 異常類。
T —— 泛型。
4.3 泛型不是協變的
關於泛型的混淆,一個常見的來源就是假設它們像數組一樣是協變的。其實它們不是協變的。List<Object> 不是 List<String> 的父類型。
如果 A 擴展 B,那麼 A 的數組也是 B 的數組,並且完全可以在需要 B[] 的地方使用 A[]:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代碼是有效的,因為一個 Integer 是一個 Number,因而一個 Integer 數組是 一個 Number 數組。但是對於泛型來說則不然。下面的代碼是無效的:
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
最初,大多數 Java 程序員覺得這缺少協變很煩人,或者甚至是“壞的(broken)”,但是之所以這樣有一個很好的原因。如果可以將 List<Integer> 賦給 List<Number>,下面的代碼就會違背泛型應該提供的類型安全:
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
numberList.add(new Float(3.1415));
因為 intList 和 numberList 都是有別名的,如果允許的話,上面的代碼就會讓您將不是 Integers 的東西放進 intList 中。但是,正如下一屏將會看到的,您有一個更加靈活的方式來定義泛型。
4.4 類型通配符
假設您具有該方法:
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
上面的代碼在 JDK 5.0 上編譯通過,但是如果試圖用 List<Integer> 調用它,則會得到警告。出現警告是因為,您將泛型(List<Integer>)傳遞給一個只承諾將它當作 List(所謂的原始類型)的方法,這將破壞使用泛型的類型安全。
如果試圖編寫像下面這樣的方法,那麼將會怎麼樣?
void printList(List<Object> l) {
for (Object o : l)
System.out.println(o);
}
它仍然不會通過編譯,因為一個 List<Integer> 不是 一個 List<Object>(正如前一屏 泛型不是協變的 中所學的)。這才真正煩人 —— 現在您的泛型版本還沒有普通的非泛型版本有用!
解決方案是使用類型通配符:
void printList(List<?> l) {
for (Object o : l)
System.out.println(o);
}
上面代碼中的問號是一個類型通配符。它讀作“問號”。List<?> 是任何泛型 List 的父類型,所以您完全可以將 List<Object>、List<Integer> 或 List<List<List<Flutzpah>>> 傳遞給 printList()。
4.5 類型通配符的作用
類型通配符中引入了類型通配符,這讓您可以聲明 List<?> 類型的變量。您可以對這樣的 List 做什麼呢?非常方便,可以從中檢索元素,但是不能添加元素。原因不是編譯器知道哪些方法修改列表哪些方法不修改列表,而是(大多數)變化的方法比不變化的方法需要更多的類型信息。下面的代碼則工作得很好:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
System.out.println(lu.get(0));
為什麼該代碼能工作呢?對於 lu,編譯器一點都不知道 List 的類型參數的值。但是編譯器比較聰明,它可以做一些類型推理。在本例中,它推斷未知的類型參數必須擴展 Object。(這個特定的推理沒有太大的跳躍,但是編譯器可以作出一些非常令人佩服的類型推理,後面就會看到(在底層細節一節中)。所以它讓您調用 List.get() 並推斷返回類型為 Object。
另一方面,下面的代碼不能工作:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.add(new Integer(43)); // error
在本例中,對於 lu,編譯器不能對 List 的類型參數作出足夠嚴密的推理,以確定將 Integer 傳遞給 List.add() 是類型安全的。所以編譯器將不允許您這麼做。
以免您仍然認為編譯器知道哪些方法更改列表的內容哪些不更改列表內容,請注意下面的代碼將能工作,因為它不依賴於編譯器必須知道關於 lu 的類型參數的任何信息:
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.clear();
4.6 泛型方法
(在 類型參數 一節中)您已經看到,通過在類的定義中添加一個形式類型參數列表,可以將類泛型化。方法也可以被泛型化,不管它們定義在其中的類是不是泛型化的。
泛型類在多個方法簽名間實施類型約束。在 List<V> 中,類型參數 V 出現在 get()、add()、contains() 等方法的簽名中。當創建一個 Map<K, V> 類型的變量時,您就在方法之間宣稱一個類型約束。您傳遞給 add() 的值將與 get() 返回的值的類型相同。
類似地,之所以聲明泛型方法,一般是因為您想要在該方法的多個參數之間宣稱一個類型約束。例如,下面代碼中的 ifThenElse() 方法,根據它的第一個參數的布爾值,它將返回第二個或第三個參數:
public <T> T ifThenElse(boolean b, T first, T second) {
return b ? first : second;
}
注意,您可以調用 ifThenElse(),而不用顯式地告訴編譯器,您想要 T 的什麼值。編譯器不必顯式地被告知 T 將具有什麼值;它只知道這些值都必須相同。編譯器允許您調用下面的代碼,因為編譯器可以使用類型推理來推斷出,替代 T 的 String 滿足所有的類型約束:
String s = ifThenElse(b, "a", "b");
類似地,您可以調用:
Integer i = ifThenElse(b, new Integer(1), new Integer(2));
但是,編譯器不允許下面的代碼,因為沒有類型會滿足所需的類型約束:
String s = ifThenElse(b, "pi", new Float(3.14));
為什麼您選擇使用泛型方法,而不是將類型 T 添加到類定義呢?(至少)有兩種情況應該這樣做:
當泛型方法是靜態的時,這種情況下不能使用類類型參數。
當 T 上的類型約束對於方法真正是局部的時,這意味著沒有在相同類的另一個方法簽名中使用相同類型 T 的約束。通過使得泛型方法的類型參數對於方法是局部的,可以簡化封閉類型的簽名。
4.7 有限制類型
在前一屏 泛型方法 的例子中,類型參數 V 是無約束的或無限制的類型。有時在還沒有完全指定類型參數時,需要對類型參數指定附加的約束。
考慮例子 Matrix 類,它使用類型參數 V,該參數由 Number 類來限制:
public class Matrix<V extends Number> { ... }
編譯器允許您創建 Matrix<Integer> 或 Matrix<Float> 類型的變量,但是如果您試圖定義 Matrix<String> 類型的變量,則會出現錯誤。類型參數 V 被判斷為由 Number 限制 。在沒有類型限制時,假設類型參數由 Object 限制。這就是為什麼前一屏 泛型方法 中的例子,允許 List.get() 在 List<?> 上調用時返回 Object,即使編譯器不知道類型參數 V 的類型。
5. 一個簡單的泛型類
5.1 編寫基本的容器類
此時,您可以開始編寫簡單的泛型類了。到目前為止,泛型類最常見的用例是容器類(比如集合框架)或者值持有者類(比如 WeakReference 或 ThreadLocal)。我們來編寫一個類,它類似於 List,充當一個容器。其中,我們使用泛型來表示這樣一個約束,即 Lhist 的所有元素將具有相同類型。為了實現起來簡單,Lhist 使用一個固定大小的數組來保存值,並且不接受 null 值。
Lhist 類將具有一個類型參數 V(該參數是 Lhist 中的值的類型),並將具有以下方法:
public class Lhist<V> {
public Lhist(int capacity) { ... }
public int size() { ... }
public void add(V value) { ... }
public void remove(V value) { ... }
public V get(int index) { ... }
}
要實例化 Lhist,只要在聲明時指定類型參數和想要的容量:
Lhist<String> stringList = new Lhist<String>(10);
5.2 實現構造函數
在實現 Lhist 類時,您將會遇到的第一個攔路石是實現構造函數。您可能會像下面這樣實現它:
public class Lhist<V> {
private V[] array;
public Lhist(int capacity) {
array = new V[capacity]; // illegal
}
}
這似乎是分配後備數組最自然的一種方式,但是不幸的是,您不能這樣做。具體原因很復雜,當學習到底層細節一節中的“擦除”主題時,您就會明白。分配後備數組的實現方式很古怪且違反直覺。下面是構造函數的一種可能的實現(該實現使用集合類所采用的方法):
public class Lhist<V> {
private V[] array;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
}
另外,也可以使用反射來實例化數組。但是這樣做需要給構造函數傳遞一個附加的參數 —— 一個類常量,比如 Foo.class。後面在 Class<T> 一節中將討論類常量。
5.3 實現方法
實現 Lhist 的方法要容易得多。下面是 Lhist 類的完整實現:
public class Lhist<V> {
private V[] array;
private int size;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
public void add(V value) {
if (size == array.length)
throw new IndexOutOfBoundsException(Integer.toString(size));
else if (value == null)
throw new NullPointerException();
array[size++] = value;
}
public void remove(V value) {
int removalCount = 0;
for (int i=0; i<size; i++) {
if (array[i].equals(value))
++removalCount;
else if (removalCount > 0) {
array[i-removalCount] = array[i];
array[i] = null;
}
}
size -= removalCount;
}
public int size() { return size; }
public V get(int i) {
if (i >= size)
throw new IndexOutOfBoundsException(Integer.toString(i));
return array[i];
}
}
注意,您在將會接受或返回 V 的方法中使用了形式類型參數 V,但是您一點也不知道 V 具有什麼樣的方法或域,因為這些對泛型代碼是不可知的。
5.4 使用 Lhist 類
使用 Lhist 類很容易。要定義一個整數 Lhist,只需要在聲明和構造函數中為類型參數提供一個實際值即可:
Lhist<Integer> li = new Lhist<Integer>(30);
編譯器知道,li.get() 返回的任何值都將是 Integer 類型,並且它還強制傳遞給 li.add() 或 li.remove() 的任何東西都是 Integer。除了實現構造函數的方式很古怪之外,您不需要做任何十分特殊的事情以使 Lhist 是一個泛型類。
6. Java類庫中的泛型
6.1 集合類
到目前為止,Java 類庫中泛型支持存在最多的地方就是集合框架。就像容器類是 C++ 語言中模板的主要動機一樣(參閱附錄 A:與 C++ 模板的比較)(盡管它們隨後用於很多別的用途),改善集合類的類型安全是 Java 語言中泛型的主要動機。集合類也充當如何使用泛型的模型,因為它們演示了泛型的幾乎所有的標准技巧和方言。
所有的標准集合接口都是泛型化的 —— Collection<V>、List<V>、Set<V> 和 Map<K,V>。類似地,集合接口的實現都是用相同類型參數泛型化的,所以 HashMap<K,V> 實現 Map<K,V> 等。
集合類也使用泛型的許多“技巧”和方言,比如上限通配符和下限通配符。例如,在接口 Collection<V> 中,addAll 方法是像下面這樣定義的:
interface Collection<V> {
boolean addAll(Collection<? extends V>);
}
該定義組合了通配符類型參數和有限制類型參數,允許您將 Collection<Integer> 的內容添加到 Collection<Number>。
如果類庫將 addAll() 定義為接受 Collection<V>,您就不能將 Collection<Integer> 的內容添加到 Collection<Number>。不是限制 addAll() 的參數是一個與您將要添加到的集合包含相同類型的集合,而有可能建立一個更合理的約束,即傳遞給 addAll() 的集合的元素 適合於添加到您的集合。有限制類型允許您這樣做,並且使用有限制通配符使您不需要使用另一個不會用在其他任何地方的占位符名稱。
應該可以將 addAll() 的類型參數定義為 Collection<V>。但是,這不但沒什麼用,而且還會改變 Collection 接口的語義,因為泛型版本的語義將會不同於非泛型版本的語義。這闡述了泛型化一個現有的類要比定義一個新的泛型類難得多,因為您必須注意不要更改類的語義或者破壞現有的非泛型代碼。
作為泛型化一個類(如果不小心的話)如何會更改其語義的一個更加微妙的例子,注意 Collection.removeAll() 的參數的類型是 Collection<?>,而不是 Collection<? extends V>。這是因為傳遞混合類型的集合給 removeAll() 是可接受的,並且更加限制地定義 removeAll 將會更改方法的語義和有用性。
6.2 其他容器類
除了集合類之外,Java 類庫中還有幾個其他的類也充當值的容器。這些類包括 WeakReference、SoftReference 和 ThreadLocal。它們都已經在其包含的值的類型上泛型化了,所以 WeakReference<T> 是對 T 類型的對象的弱引用,ThreadLocal<T> 則是到 T 類型的線程局部變量的句柄。
6.3 泛型不止用於容器
泛型最常見最直觀的使用是容器類,比如集合類或引用類(比如 WeakReference<T>)。Collection<V> 中類型參數的含義很明顯 —— “一個所有值都是 V 類型的集合”。類似地,ThreadLocal<T> 也有一個明顯的解釋 —— “一個其類型是 T 的線程局部變量”。但是,泛型規格說明中沒有指定容積。
像 Comparable<T> 或 Class<T> 這樣的類中類型參數的含義更加微妙。有時,就像 Class<T> 中一樣,類型變量主要是幫助編譯器進行類型推理。有時,就像隱含的 Enum<E extends Enum<E>> 中一樣,類型變量只是在類層次結構上加一個約束。
6.3.1 Comparable<T>
Comparable 接口已經泛型化了,所以實現 Comparable 的對象聲明它可以與什麼類型進行比較。(通常,這是對象本身的類型,但是有時也可能是父類。)
public interface Comparable<T> {
public boolean compareTo(T other);
}
所以 Comparable 接口包含一個類型參數 T,該參數是一個實現 Comparable 的類可以與之比較的對象的類型。這意味著如果定義一個實現 Comparable 的類,比如 String,就必須不僅聲明類支持比較,還要聲明它可與什麼比較(通常是與它本身比較):
public class String implements Comparable<String> { ... }
現在來考慮一個二元 max() 方法的實現。您想要接受兩個相同類型的參數,二者都是 Comparable,並且相互之間是 Comparable。幸運的是,如果使用泛型方法和有限制類型參數的話,這相當直觀:
public static <T extends Comparable<T>> T max(T t1, T t2) {
if (t1.compareTo(t2) > 0)
return t1;
else
return t2;
}
在本例中,您定義了一個泛型方法,在類型 T 上泛型化,您約束該類型擴展(實現) Comparable<T>。兩個參數都必須是 T 類型,這表示它們是相同類型,支持比較,並且相互可比較。容易!
更好的是,編譯器將使用類型推理來確定當調用 max() 時 T 的值表示什麼意思。所以根本不用指定 T,下面的調用就能工作:
String s = max("moo", "bark");
編譯器將計算出 T 的預定值是 String,因此它將進行編譯和類型檢查。但是如果您試圖用不實現 Comparable<X> 的 類 X 的參數調用 max(),那麼編譯器將不允許這樣做。
6.3.2 Class<T>
類 Class 已經泛型化了,但是很多人一開始都感覺其泛型化的方式很混亂。Class<T> 中類型參數 T 的含義是什麼?事實證明它是所引用的類接口。怎麼會是這樣的呢?那是一個循環推理?如果不是的話,為什麼這樣定義它?
在以前的 JDK 中,Class.newInstance() 方法的定義返回 Object,您很可能要將該返回類型強制轉換為另一種類型:
class Class {
Object newInstance();
}
但是使用泛型,您定義 Class.newInstance() 方法具有一個更加特定的返回類型:
class Class<T> {
T newInstance();
}
如何創建一個 Class<T> 類型的實例?就像使用非泛型代碼一樣,有兩種方式:調用方法 Class.forName() 或者使用類常量 X.class。Class.forName() 被定義為返回 Class<?>。另一方面,類常量 X.class 被定義為具有類型 Class<X>,所以 String.class 是 Class<String> 類型的。
URL:http://www.bianceng.cn/Programming/Java/201608/50385.htm
讓 Foo.class 是 Class<Foo> 類型的有什麼好處?大的好處是,通過類型推理的魔力,可以提高使用反射的代碼的類型安全。另外,還不需要將 Foo.class.newInstance() 強制類型轉換為 Foo。
考慮一個方法,它從數據庫檢索一組對象,並返回 JavaBeans 對象的一個集合。您通過反射來實例化和初始化創建的對象,但是這並不意味著類型安全必須完全被拋至腦後。考慮下面這個方法:
public static<T> List<T> getRecords(Class<T> c, Selector s) {
// Use Selector to select rows
List<T> list = new ArrayList<T>();
for (/* iterate over results */) {
T row = c.newInstance();
// use reflection to set fields from result
list.add(row);
}
return list;
}
可以像下面這樣簡單地調用該方法:
List<FooRecord> l = getRecords(FooRecord.class, fooSelector);
編譯器將會根據 FooRecord.class 是 Class<FooRecord> 類型的這一事實,推斷 getRecords() 的返回類型。您使用類常量來構造新的實例並提供編譯器在類型檢查中要用到的類型信息。
6.3.3 用 Class<T> 替換 T[]
Collection 接口包含一個方法,用於將集合的內容復制到一個調用者指定類型的數組中:
public Object[] toArray(Object[] prototypeArray) { ... }
toArray(Object[]) 的語義是,如果傳遞的數組足夠大,就會使用它來保存結果,否則,就會使用反射分配一個相同類型的新數組。一般來說,單獨傳遞一個數組作為參數來提供想要的返回類型是一個小技巧,但是在引入泛型之前,這是與方法交流類型信息最方便的方式。
有了泛型,就可以用一種更加直觀的方式來做這件事。不像上面這樣定義 toArray(),泛型 toArray() 可能看起來像下面這樣:
public<T> T[] toArray(Class<T> returnType)
調用這樣一個 toArray() 方法很簡單:
FooBar[] fba = something.toArray(FooBar.class);
Collection 接口還沒有改變為使用該技術,因為這會破壞許多現有的集合實現。但是如果使用泛型從新構建 Collection,則當然會使用該方言來指定它想要返回值是哪種類型。
6.3.4 Enum<E>
JDK 5.0 中 Java 語言另一個增加的特性是枚舉。當您使用 enum 關鍵字聲明一個枚舉時,編譯器就會在內部為您生成一個類,用於擴展 Enum 並為枚舉的每個值聲明靜態實例。所以如果您說:
public enum Suit {HEART, DIAMOND, CLUB, SPADE};
編譯器就會在內部生成一個叫做 Suit 的類,該類擴展 java.lang.Enum<Suit> 並具有叫做 HEART、DIAMOND、CLUB 和 SPADE 的常量(public static final)成員,每個成員都是 Suit 類。
與 Class 一樣,Enum 也是一個泛型類。但是與 Class 不同,它的簽名稍微更復雜一些:
class Enum<E extends Enum<E>> { . . . }
這究竟是什麼意思?這難道不會導致無限遞歸?
我們逐步來分析。類型參數 E 用於 Enum 的各種方法中,比如 compareTo() 或 getDeclaringClass()。為了這些方法的類型安全,Enum 類必須在枚舉的類上泛型化。
所以 extends Enum<E> 部分如何理解?該部分又具有兩個部分。第一部分指出,作為 Enum 的類型參數的類本身必須是 Enum 的子類型,所以您不能聲明一個類 X 擴展 Enum<Integer>。第二部分指出,任何擴展 Enum 的類必須傳遞它本身 作為類型參數。您不能聲明 X 擴展 Enum<Y>,即使 Y 擴展 Enum。
總之,Enum 是一個參數化的類型,只可以為它的子類型實例化,並且這些子類型然後將根據子類型來繼承方法。幸運的是,在 Enum 情況下,編譯器為您做這些工作,一切都很好。
6.3.5 與非泛型代碼相互操作
數百萬行現有代碼使用已經泛型化的 Java 類庫中的類,比如集合框架、Class 和 ThreadLocal。JDK 5.0 中的改進不要破壞所有這些代碼是很重要的,所以編譯器允許您在不指定其類型參數的情況下使用泛型類。
當然,以“舊方式”做事沒有新方式安全,因為忽略了編譯器准備提供的類型安全。如果您試圖將 List<String> 傳遞給一個接受 List 的方法,它將能夠工作,但是編譯器將會發出一個可能喪失類型安全的警告,即所謂的“unchecked conversion(不檢查轉換)”警告。
沒有類型參數的泛型,比如聲明為 List 類型而不是 List<Something> 類型的變量,叫做原始類型。原始類型與參數化類型的任何實例化是賦值兼容的,但是這樣的賦值會生成 unchecked-conversion 警告。
為了消除一些 unchecked-conversion 警告,假設您不准備泛型化所有的代碼,您可以使用通配符類型參數。使用 List<?> 而不使用 List。List 是原始類型;List<?> 是具有未知類型參數的泛型。編譯器將以不同的方式對待它們,並很可能發出更少的警告。
無論在哪種情況下,編譯器在生成字節碼時都會生成強制類型轉換,所以生成的字節碼在每種情況下都不會比沒有泛型時更不安全。如果您設法通過使用原始類型或類文件來破壞類型安全,就會得到與不使用泛型時得到的相同的 ClassCastException 或 ArrayStoreException。
7. Java 泛型的理解與等價實現
泛型是JAVA SE 1.5的新特性,泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。
JAVA語言引入泛型的好處是安全簡單。
在JAVA SE 1.5之前,沒有泛型的情況的下,通過對類型Object的引用來實現參數的“任意化”,“任意化”帶來的缺點是要做顯式的強制類型轉換,而這種轉換是要求開發者對實際參數類型可以預知的情況下進行的。對於強制類型轉換錯誤的情況,編譯器可能不提示錯誤,在運行的時候才出現異常,這是一個安全隱患。
泛型的好處是在編譯的時候檢查類型安全,並且所有的強制轉換都是自動和隱式的,提高代碼的重用率。
泛型在使用中還有一些規則和限制:
1、泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型。
2、同一種泛型可以對應多個版本(因為參數類型是不確定的),不同版本的泛型類實例是不兼容的。
3、泛型的類型參數可以有多個。
4、泛型的參數類型可以使用extends語句,例如<T extends superclass>。習慣上成為“有界類型”。
5、泛型的參數類型還可以是通配符類型。例如Class<?> classType = Class.forName(java.lang.String);
泛型還有接口、方法等等,內容很多,需要花費一番功夫才能理解掌握並熟練應用。在此給出我曾經了解泛型時候寫出的兩個例子(根據看的印象寫的),實現同樣的功能,一個使用了泛型,一個沒有使用,通過對比,可以很快學會泛型的應用,學會這個基本上學會了泛型70%的內容。
估計你快等不及了,現在就貼出源碼:
例子一:使用了泛型
public class Gen<T> { private T ob; //定義泛型成員變量 public Gen(T ob) { this.ob = ob; } public T getOb() { return ob; } public void setOb(T ob) { this.ob = ob; } public void showTyep() { System.out.println("T的實際類型是: " + ob.getClass().getName()); } } public class GenDemo { public static void main(String[] args){ //定義泛型類Gen的一個Integer版本 Gen<Integer> intOb=new Gen<Integer>(88); intOb.showTyep(); int i= intOb.getOb(); System.out.println("value= " + i); System.out.println("----------------------------------"); //定義泛型類Gen的一個String版本 Gen<String> strOb=new Gen<String>("Hello Gen!"); strOb.showTyep(); String s=strOb.getOb(); System.out.println("value= " + s); } }
例子二:沒有使用泛型
public class Gen2 { private Object ob; //定義一個通用類型成員 public Gen2(Object ob) { this.ob = ob; } public Object getOb() { return ob; } public void setOb(Object ob) { this.ob = ob; } public void showTyep() { System.out.println("T的實際類型是: " + ob.getClass().getName()); } } public class GenDemo2 { public static void main(String[] args) { //定義類Gen2的一個Integer版本 Gen2 intOb = new Gen2(new Integer(88)); intOb.showTyep(); int i = (Integer) intOb.getOb(); System.out.println("value= " + i); System.out.println("----------------------------------"); //定義類Gen2的一個String版本 Gen2 strOb = new Gen2("Hello Gen!"); strOb.showTyep(); String s = (String) strOb.getOb(); System.out.println("value= " + s); } }
8. Java5泛型的用法,T.class的獲取
Java 5的泛型語法已經有太多書講了,這裡不再打字貼書。GP一定有用,不然Java和C#不會約好了似的同時開始支持GP。但大家也清楚,GP和Ruby式的動態OO語言屬於不同的意識形態,如果是一人一票,我想大部分的平民程序員更熱衷動態OO語言的平白自然。但如果不准備跳槽到支持JSR223的動態語言,那還是看看GP吧。
胡亂總結泛型的四點作用:
第一是泛化,可以拿個T代表任意類型。但GP是被C++嚴苛的靜態性逼出來的,落到Java、C#這樣的花語平原裡----所有對象除幾個原始類型外都派生於Object,再加上Java的反射功能,Java的Collection庫沒有范型一樣過得好好的。
第二是泛型 + 反射,原本因為Java的泛型拿不到T.class而覺得泛型沒用,最近才剛剛學到通過反射的API來獲取T的Class,後述。
第三是收斂,就是增加了類型安全,減少了強制類型轉換的代碼。這點倒是Java Collection歷來的弱項。
第四是可以在編譯期搞很多東西,比如MetaProgramming。但除非能完全封閉於框架內部,框架的使用者和擴展者都不用學習這些東西的用法,否則那就是自絕於人民的票房毒藥。C++的MetaProgramming好厲害吧,但對比一下Python拿Meta Programming生造一個Class出來的簡便語法,就明白什麼才是真正的叫好又叫座。
所以,作為一個架構設計師,應該使用上述的第2,3項用法,在框架類裡配合使用反射和泛型,使得框架的能力更強; 同時采用收斂特性,本著對人民負責的精神,用泛型使框架更加類型安全,更少強制類型轉換。
擦拭法避免了Java的流血分裂 :
大家經常罵Java GP的擦拭法實現,但我覺得多虧於它的中庸特性---如果你用就是范型,不用就是普通Object,避免了Java陣營又要經歷一場to be or not to be的分裂。
最大的例子莫過Java 5的Collection 框架, 比如有些同學堅持認為自己不會白癡到類型出錯,而且難以忍受每個定義的地方都要帶一個泛型定義List〈Book〉,不用強制類型轉換所省下的代碼還不夠N處定義花的(對了,java裡面還沒有tyepdef.....),因此對范型十分不感冒,這時就要齊齊感謝這個搽拭法讓你依然可以對一個泛型框架保持非泛型的用法了...
通過反射獲得 T.class:
不知為何書上不怎麼講這個,是差沙告訴我才知道的,最經典的應用見Hibernate wiki的Generic Data Access Objects, 代碼如下
abstract public class BaseHibernateEntityDao<T> extends HibernateDaoSupport { private Class<T> entityClass; public BaseHibernateEntityDao() { entityClass =(Class<T>) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0]; } public T get(Serializable id) { T o = (T) getHibernateTemplate().get(entityClass, id); } }
精華就是這句了:
Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
泛型之後,所有BaseHibernateEntityDao的子類只要定義了泛型,就無需再重載getEnttityClass(),get()函數和find()函數,銷益挺明顯的,所以SpringSide的Dao基類毫不猶豫就泛型了。
不過擦拭法的大棒仍在,所以子類的泛型語法可不能亂寫,最正確的用法只有:
public class BookDao extends BaseHibernateEntityDao<Book>
9. Java 5.0泛型編程之泛型類型
Java5.0的新特性之一是引入了泛型類型和泛型方法。一個泛型類型通過使用一個或多個類型變量來定義,並擁有一個或多個使用一個類型變量作為一個參數或者返回值的占位符。例如,類型java.util.List<E>是一個泛型類型:一個list,其元素的類型被占位符E描述。這個類型有一個名為add()的方法,被聲明為有一個類型為E的參數,同時,有一個get()方法,返回值被聲明為E類型。
使用泛型類型,你應該為類型變量詳細指明實際的類型,形成一個就像List<String>類似的參數化類型。[1]指明這些額外的類型信息的原因是編譯器據此能夠在編譯期為您提供很強的類型檢查,增強您的程序的類型安全性。舉個例子來說,您有一個只能保持String對象的List,那麼這種類型檢查就能夠阻止您往裡面加入String[]對象。同樣的,增加的類型信息使編譯器能夠為您做一些類型轉換的事情。比如,編譯器知道了一個List<String>有個get()方法,其返回值是一個String對象,因此您不再需要去將返回值由一個Object強制轉換為String。
Java.util包中的集合類在java5.0中已經被做成了泛型,也許您將會在您的程序中頻繁的使用到他們。類型安全的集合類就是一個泛型類型的典型案例。即便您從沒有定義過您自己的泛型類型甚至從未用過除了java.util中的集合類以外的泛型類型,類型安全的集合類的好處也是極有意義的一個標志——他們證明了這個主要的新語言特性的復雜性。
我們從探索類型安全的集合類中的基本的泛型用法開始,進而研究更多使用泛型類型的復雜細節。然後我們討論類型參數通配符和有界通配符。描繪了如何使用泛型以後,我們闡明如何編寫自己的泛型類型和泛型方法。我們對於泛型的討論將結束於一趟對於JavaAPI的核心中重要的泛型類型的旅行。這趟旅程將探索這些類型以及他們的用法,旅程的目的是為了讓您對泛型如何工作這個問題有個深入的理解。
類型安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對象的set、對象的list以及基於key-value的map。第五章將談到集合類。這裡,我們討論的是在java5.0中集合類使用類型參數來界定集合中的對象的類型。這個討論並不適合java1.4或更早期版本。如果沒有泛型,對於集合類的使用需要程序員記住每個集合中元素的類型。當您在java1.4種創建了一個集合,您知道您放入到集合中的對象的類型,但是編譯器不知道。您必須小心地往其中加入一個合適類型的元素,當需要從集合中獲取元素時,您必須顯式的寫強制類型轉換以將他們從Object轉換為他們真是的類型。考察下邊的java1.4的代碼。
public static void main(String[] args) {
// This list is intended to hold only strings.
// The compiler doesn't know that so we have to remember ourselves.
List wordlist = new ArrayList();
// Oops! We added a String[] instead of a String.
// The compiler doesn't know that this is an error.
wordlist.add(args);
// Since List can hold arbitrary objects, the get() method returns
// Object. Since the list is intended to hold strings, we cast the
// return value to String but get a ClassCastException because of
// the error above.
String word = (String)wordlist.get(0);
}
泛型類型解決了這段代碼中的顯示的類型安全問題。Java.util中的List或是其他集合類已經使用泛型重寫過了。就像前面提到的, List被重新定義為一個list,它中間的元素類型被一個類型可變的名稱為E的占位符描述。Add()方法被重新定義為期望一個類型為E的參數,用於替換以前的Object,get()方法被重新定義為返回一個E,替換了以前的Object。
在java5.0中,當我們申明一個List或者創建一個ArrayList的實例的時候,我們需要在泛型類型的名字後面緊跟一對“<>”,尖括號中寫入我們需要的實際的類型。比如,一個保持String的List應該寫成“List<String>”。需要注意的是,這非常象給一個方法傳一個參數,區別是我們使用類型而不是值,同時使用尖括號而不是圓括號
Java.util的集合類中的元素必須是對象化的,他們不能是基本類型。泛型的引入並沒有改變這點。泛型不能使用基本類型:我們不能這樣來申明——Set<char>或者List<int>。記住,無論如何,java5.0中的自動打包和自動解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣方便。
在Java5.0中,上面的例子將被重寫為如下方式:
public static void main(String[] args) {
// This list can only hold String objects
List<String> wordlist = new ArrayList<String>();
// args is a String[], not String, so the compiler won't let us do this
wordlist.add(args); // Compilation error!
// We can do this, though.
// Notice the use of the new for/in looping statement
for(String arg : args) wordlist.add(arg);
// No cast is required. List<String>.get() returns a String.
String word = wordlist.get(0);
}
值得注意的是代碼量其實並沒有比原來那個沒有泛型的例子少多少。使用“(String)”這樣的類型轉換被替換成了類型參數“<String>”。不同的是類型參數需要且僅需要聲明一次,而list能夠被使用任何多次,不需要類型轉換。在更長點的例子代碼中,這一點將更加明顯。即使在那些看上去泛型語法比非泛型語法要冗長的例子裡,使用泛型依然是非常有價值的——額外的類型信息允許編譯器在您的代碼裡執行更強的錯誤檢查。以前只能在運行起才能發現的錯誤現在能夠在編譯時就被發現。此外,以前為了處理類型轉換的異常,我們需要添加額外的代碼行。如果沒有泛型,那麼當發生類型轉換異常的時候,一個ClassCastException異常就會被從實際代碼中拋出。
就像一個方法可以使用任意數量的參數一樣,類允許使用多個類型變量。接口Java.util.Map就是一個例子。一個Map體現了從一個key的對象到一個value的對象的映射關系。接口Map申明了一個類型變量來描述key的類型而另一個類型變量來描述value的類型。舉個例子來說,假設您希望做一個String對象到Integer對象的映射關系:
public static void main(String[] args) {
// A map from strings to their position in the args[] array
Map<String,Integer> map = new HashMap<String,Integer>();
// Note that we use autoboxing to wrap i in an Integer object.
for(int i=0; i < args.length; i++)
map.put(args[i], i);
// Find the array index of a word. Note no cast is required!
Integer position = map.get("hello");
// We can also rely on autounboxing to convert directly to an int,
// but this throws a NullPointerException if the key does not exist in the map
int pos = map.get("world");
}
象List<String>這個一個參數類型其本身也是也一個類型,也能夠被用於當作其他類型的一個類型變量值。您可能會看到這樣的代碼:
// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting. We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here's how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
在上面的代碼裡,java.util.List<E>和java.util.Map<K,V>的get()方法返回一個類型為E的list元素或者一個類型為V的map元素。注意,無論如何,泛型類型能夠更精密的使用他們的變量。在本書中的參考章節查看List<E>,您將會看到它的iterator( )方法被聲明為返回一個Iterator<E>。這意味著,這個方法返回一個跟list的實際的參數類型一樣的一個參數類型的實例。為了具體的說明這點,下面的例子提供了不使用get(0)方法來獲取一個List<String>的第一個元素的方法。
List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
10. 理解泛型類型
本段將對泛型類型的使用細節做進一步的探討,以嘗試說明下列問題:
不帶類型參數的使用泛型的後果
參數化類型的體系
一個關於編譯期泛型類型的類型安全的漏洞和一個用於確保運行期類型安全的補丁
為什麼參數化類型的數組不是類型安全的
未經處理的類型和不被檢查的警告
即使被重寫的Java集合類帶來了泛型的好處,在使用他們的時候您也不被要求說明類型變量。一個不帶類型變量的泛型類型被認為是一個未經處理的類型(raw type)。這樣,5.0版本以前的java代碼仍然能夠運行:您顯式的編寫所有類型轉換就像您已經這樣寫的一樣,您可能會被一些來自編譯器的麻煩所困擾。查看下列存儲不同類型的對象到一個未經處理的List:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
這段代碼在java1.4下運行得很好。如果您用java5.0來編譯它,javac編譯了,但是會打印出這樣的“抱怨”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint參數後重新編譯,我們會看到這些警告:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add("hello");
Test.java:7: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add(new Integer(123));
編譯在add()方法的調用上給出了警告,因為它不能夠確信加入到list中的值具有正確的類型。它告訴我們說我們使用了一個未經處理的類型,它不能驗證我們的代碼是類型安全的。注意,get()方法的調用是沒有問題的,因為能夠被獲得的元素已經安全的存在於list中了。
如果您不想使用任何的java5.0的新特性,您可以簡單的通過帶-source1.4標記來編譯他們,這樣編譯器就不會再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,通過使用一個
“@SuppressWarnings("unchecked
")”注解(查看本章的4.3節)隱瞞這些警告信息或者升級您的代碼,加入類型變量描述。[2]下列示例代碼,編譯的時候不再會有警告但仍然允許您往list中放入不同的類型的對象。
List<Object> l = new ArrayList<Object>();
l.add("hello");
l.add(123); // autoboxing
Object o = l.get(0);
參數化類型的體系
參數化類型有類型體系,就像一般的類型一樣。這個體系基於對象的類型,而不是變量的類型。這裡有些例子您可以嘗試:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l; // okay
Collection<Integer> n = l; // okay
ArrayList<Number> o = l; // error
Collection<Object> p = (Collection<Object>)l; // error, even with cast
一個List<Integer>是一個Collection<Integer>,但不是一個List<Object>。這句話不容易理解,如果您想理解為什麼泛型這樣做,這段值得看一下。考察這段代碼:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile. But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1); // Can't cast a String to Integer!
這就是為什麼List<Integer>不是一個List<Object>的原因,雖然List<Integer>中所有的元素事實上是一個Object的實例。如果允許轉換成List<Object>,那麼轉換後,理論上非整型的對象也將被允許添加到list中。
1
運行時類型安全
就像我們所見到的,一個List<X>不允許被轉換為一個List<Y>,即使這個X能夠被轉換為Y。然而,一個List<X>能夠被轉換為一個List,這樣您就可以通過繼承的方法來做這樣的事情。
這種將參數化類型轉換為非參數化類型的能力對於向下兼容是必要的,但是它會在泛型所帶來的類型安全體系上鑿個
漏洞
:
// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;
// This line is a bug, but it compiles and runs.
// The
Java
5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we'd never even get the warning.
l.add("hello");
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
泛型僅提供了編譯期的類型安全。如果您使用java5.0的編譯器來編譯您的代碼並且沒有得到任何警告,這些編譯器的檢查能夠確保您的代碼在運行期也是類型安全的。如果您獲得了警告或者使用了像未經處理的類型那樣修改您的集合的代碼,那麼您需要增加一些步驟來確保運行期的類型安全。您可以通過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個wrapper集合,從而在運行時檢查確認只有正確類型的值能夠被置入集合眾。下面是一個能夠補上類型安全漏洞的一個例子:
// Here's a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");
參數化類型的數組
在使用泛型類型的時候,數組需要特別的考慮。回憶一下,如果T是S的父類(或者接口),那麼類型為S的數組S[],同時又是類型為T的數組T[]。正因為如此,每次您存放一個對象到數組中時,Java解釋器都必須進行檢查以確保您放入的對象類型與要存放的數組所允許的類型是匹對的。例如,下列代碼在運行期會檢查失敗,拋出一個ArrayStoreException異常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
雖然編譯時obj是一個Object[],但是在運行時它是一個String[],它不允許被用於存放一個Integer。
當我們使用泛型類型的時候,僅僅依靠運行時的數組存放異常檢查是不夠的,因為一個運行時進行的檢查並不能夠獲取編譯時的類型參數信息。查看下列代碼:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali; // No ArrayStoreException
String s = wordlists[0].get(0); // ClassCastException!
如果上面的代碼被允許,那麼運行時的數組存儲檢查將會成功:沒有編譯時的類型參數,代碼簡單地存儲一個ArrayList到一個ArrayList[]數組,非常正確。既然編譯器不能阻止您通過這個方法來戰勝類型安全,那麼它轉而阻止您創建一個參數化類型的數組。所以上述情節永遠不會發生,編譯器在第一行就開始拒絕編譯了。
注意這並不是一個在使用數組時使用泛型的全部的約束,這僅僅是一個創建一個參數化類型數組的約束。我們將在學習如何寫泛型方法時再來討論這個話題。
類型參數通配符
假設我們需要寫一個方法來顯示一個List中的元素。[3]在以前,我們只需要象這樣寫段代碼:
public static void printList(PrintWriter out, List list) { for(int i=0, n=list.size(); i < n; i++) { if (i > 0) out.print(", "); out.print(list.get(i).toString()); } }
在Java5.0中,List是一個泛型類型,如果我們試圖編譯這個方法,我們將會得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個方法:
public static void printList(PrintWriter out, List<Object> list) { for(int i=0, n=list.size(); i < n; i++) { if (i > 0) out.print(", "); out.print(list.get(i).toString()); } }
這段代碼能夠編譯通過同時不會有警告,但是它並不是非常地有效,因為只有那些被聲明為List<Object>的list才會被允許使用這個方法。還記得麼,類似於List<String>和List<Integer>這樣的List並不能被轉型為List<Object>。事實上我們需要一個類型安全的printList()方法,它能夠接受我們傳入的任何List,而不關心它被參數化為什麼。解決辦法是使用類型參數通配符。方法可以被修改成這樣:
public static void printList(PrintWriter out, List<?> list) { for(int i=0, n=list.size(); i < n; i++) { if (i > 0) out.print(", "); Object o = list.get(i); out.print(o.toString()); } }
這個版本的方法能夠被編譯過,沒有警告,而且能夠在任何我們希望使用的地方使用。通配符“?”表示一個未知類型,類型List<?>被讀作“List of unknown”
作為一般原則,如果類型是泛型的,同時您並不知道或者並不關心值的類型,您應該使用“?”通配符來代替一個未經處理的類型。未經處理的類型被允許僅是為了向下兼容,而且應該只能夠被允許出現在老的代碼中。注意,無論如何,您不能在調用構造器時使用通配符。下面的代碼是非法的:
List<?> l = new ArrayList<?>();
創建一個不知道類型的List是毫無道理的。如果您創建了它,那麼您必須知道它將保持的元素是什麼類型的。您可以在隨後的方法中不關心元素類型而去遍歷這裡list,但是您需要在您創建它的時候描述元素的類型。如果你確實需要一個List來保持任何類型,那麼您只能這麼寫:
List<Object> l = new ArrayList<Object>();
從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個未經處理的List。一個使用通配符的List<?>有兩個重要的特性。第一,考察類似於get()的方法,他們被聲明返回一個值,這個值的類型是類型參數中指定的。在這個例子中,類型是“unknown”,所以這些方法返回一個Object。既然我們期望的是調用這個object的toString()方法,程序能夠很好的滿足我們的意願。
第二,考察List的類似add()的方法,他們被聲明為接受一個參數,這個參數被類型參數所定義。出人意料的是,當類型參數是未確定的,編譯器不允許您調用任何有不確定參數類型的方法——因為它不能確認您傳入了一個恰當的值。一個List(?)實際上是只讀的——既然編譯器不允許我們調用類似於add(),set(),addAll()這類的方法。
界定通配符
讓我們在我們原來的例子上作些小小的稍微復雜一點的改動。假設我們希望寫一個sumList()方法來計算list中Number類型的值的合計。在以前,我們使用未經處理的List,但是我們不想放棄類型安全,同時不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能調用List<Integer>、List<Double>中的方法了,而事實上我們需要調用。如果我們使用通配符,那麼我們實際上不能得到我們期望的類型安全,我們不能確定我們的方法被什麼樣的List所調用,Number?還是Number的子類?甚至,String?這樣的一個方法也許會被寫成這樣:
public static double sumList(List<?> list) { double total = 0.0; for(Object o : list) { Number n = (Number) o; // A cast is required and may fail total += n.doubleValue(); } return total; }
要修改這個方法讓它變得真正的類型安全,我們需要使用界定通配符(bounded wildcard),能夠確保List的類型參數是未知的,但又是Number或者Number的子類。下面的代碼才是我們想要的:
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for(Number n : list) total += n.doubleValue();
return total;
}
類型List<? extends Number>可以被理解為“Number未知子類的List”。理解這點非常重要,在這段文字中,Number被認為是其自身的子類。
注意,這樣的話,那些類型轉換已經不再需要了。我們並不知道list中元素的具體類型,但是我們知道他們能夠向上轉型為Number,因此我們可以把他們從list中把他們當作一個Number對象取出。使用一個for/in循環能夠稍微封裝一下從list中取出元素的過程。普遍性的原則是當您使用一個界定通配符時,類似於List中的get()方法的那些方法將返回一個類型為上界的值。因此如果我們在for/in循環中調用list.get(),我們將得到一個Number。在前一節說到使用通配符時類似於list.add()這種方法中的限制依然有效:舉個例子來說,如果編譯器允許我們調用這類方法,我們就可以將一個Integer放到一個聲明為僅保持Short值的list中去。
同樣可行的是使用下界通配符,不同的是用super替換extends。這個技巧在被調用的方法上有一點不同的作用。在實際應用中,下界通配符要比上界通配符用得少。我們將在後面的章節裡討論這個問題。