Java核心技術之泛型
- 泛型類(generic class)是帶有一個或者多個型別形參(type parameter)的類。
- 泛型方法是帶有型別形參的方法。
- 可以要求型別形參必須是一個或多個型別的子型別。
- 泛型類不是協變的(invariant):當S是T的子型別時,
G<S>
和G<T>
沒有任何關係。 - 通過使用通配型別形參(wildcards)
G<? extends T>
或者G<? super T>
,使得一個方法可以接受使用T
的子類或者超類例項化(instantiation)的泛型型別。 - 泛型類和泛型方法在編譯時,其型別形參將被擦除(erase)。
- 由於擦除(erasure)機制的存在,泛型的使用有很多限制(restriction)
Class<T>
是一個泛型類,這相當有用,因為newInstance
方法可以返回T
。- 即便泛型類和方法在虛擬機器中執行時已被擦除過了,你仍然可以知道它們是如何宣告的。
泛型類
如ArrayList<T>
是一個泛型類(generic class),T
被稱為型別形參(type parameter)。泛型類如下定義:
public class Entry<K, V> {
private K key;
private V value ;
public Entry(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
泛型方法
定義如下:
public class Arrays {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
型別限定(bound)
如下,型別形參T
必須要實現Comparable
和Serializable
介面:
class Interval<T extends Comparable & Serializable> implements Serializable {
}
型別變化(variance)和通配(wildcards)
假設需要實現一個方法,用於處理Employee的子物件陣列。只需要如下宣告方法即可:
public static void process(Employee[] staff) { … }
例如,Manager是Employee的子型別,你只需要傳遞一個Manager[]
給該方法即可。這種行為稱之為協變(covariance),也就是說陣列與其元素一同變化。
現在假設你要處理一個ArrayList,問題就出現了:ArrayList<Manager>
並不是ArrayList<Employee>
的子型別。
Java語言施加這種限制是有道理的,如果可以將ArrayList<Manager>
物件賦值給ArrayList<Employee>
型別的變數,那麼會造成如下的問題:
ArrayList<Manager> bosses = new ArrayList<>();
ArrayList<Employee> empls = bosses; // Not legal, but suppose it is…
empls.add(new Employee(…)); // A nonmanager in bosses!
Java裡面使用了wildcards來指定這種允許變化的方法形參和返回值。
子類通配
如下:
public static void printNames(ArrayList<? extends Employee> staff) {
for (int i = 0; i < staff.size(); i++) {
Employee e = staff.get(i);
System.out.println(e.getName());
}
}
父類通配
? super Employee
這種wildcards通常被用於函式型物件。
如:
public static void printAll(Employee[] staff, Predicate<? super Employee> filter) {
for (Employee e : staff)
if (filter.test(e))
System.out.println(e.getName());
}
仔細觀察方法呼叫filter.test(e)
。既然test方法可以處理Employee的父型別,那麼將Employee型別的物件傳遞給它是安全的。這種情形是很普遍的。函式型物件天然就與其形參型別逆變(contravariant)。當一個函式能夠處理employee物件時,那麼提供一個能處理任意型別物件的函式當然是沒問題的。
一般的,當你需要指定一個泛型的函式式介面作為方法的形參時,你應該使用super wildcard。
一些程式設計師喜歡用”PECS”(producer extends,consumer super)來幫助記憶何時使用
extends
,何時使用super
。例如,一個從中讀取資料的ArrayList是producer,而Predicate用於判斷你提供的資料,那麼就是consumer。
使用型別變數的通配
考慮Collections.sort
方法的定義:
public static <T extends Comparable<? super T>> void sort(List<T> list)
型別形參T
指定了Comparable介面的compareTo方法的實參型別。那麼為什麼不直接定義成下面這樣:
public static <T extends Comparable<T>> void sort(List<T> list)
這樣的話太過限制。假設Employee類實現了Comparable,然後Manager類繼承Employee。這樣Manager類實現了Comparable,而沒有實現Comparable,那麼Manager就不是Comparable的子類,但是它是Comparable
未作限制的通配
如下:
public static boolean hasNulls(ArrayList<?> elements) {
for (Object e : elements) {
if (e == null) return true;
}
return false;
}
當然,上面的方法完全可以使用一個泛型方法來替代:
public static <T> boolean hasNulls(ArrayList<T> elements)
但是通配型別的方式更加容易理解。
通配捕獲
通配型別(?)可以用作型別實參,但是不能用於宣告變數,如下:
public static void swap(ArrayList<?> elements, int i, int j) {
? temp = elements.get(i); // Won’t work
elements.set(i, elements.get(j));
elements.set(j, temp);
}
這樣的情況可以使用一個輔助方法來捕獲這個通配型別,如下:
public static void swap(ArrayList<?> elements, int i, int j) {
swapHelper(elements, i, j);
}
private static <T> void swapHelper(ArrayList<T> elements, int i, int j) {
T temp = elements.get(i);
elements.set(i, elements.get(j));
elements.set(j, temp);
}
JVM中的泛型
泛型對於虛擬機器是不可見的,僅僅作用於編譯階段。編譯後,型別變數被替換。沒有限定的型別變數替換為Object,有限定的情況下被替換為第一個限定型別。
因此在編譯泛型程式碼的時候,編譯器會插入強制型別轉換或者橋方法。
插入型別轉換
Entry<String, Integer> entry = …;
String key = entry.getKey();
上面的getKey方法在經過擦除後返回是Object,因而編譯器會生成類似如下的程式碼:
String key = (String) entry.getKey();
橋方法(bridge method)
當編譯器在擦除方法的形參和返回型別時,有時需要合成bridge method。如下:
public class WordList extends ArrayList<String> {
public void add(String e) {
return isBadWord(e) ? false : super.add(e);
}
…
}
再考慮下面的程式碼片段:
WordList words = …;
ArrayList<String> strings = words; // OK—conversion to superclass
strings.add("C++");
呼叫ArrayList的add方法時,由於多型機制的存在,最終呼叫的方法是WordList類的add方法。然而多型能夠正常工作,依賴於編譯器合成的bridge method。因為ArrayList在經過擦除後,其add方法接受的引數是一個Object,因此WordList類的add方法並沒有真正Override,所以多型機制是不能工作的。除非編譯器在WordList類中生成一個橋方法如下:
public void add(Object e) {
add((String) e);
}
當方法的返回型別變化時,橋方法依然有效,如下:
public class WordList extends ArrayList<String> {
public String get(int i) {
return super.get(i).toLowerCase();
}
…
}
此時WordList類中有兩個get方法,其中第二個是編譯器合成的:
String get(int) // Defined in WordList
Object get(int) // Overrides the method defined in ArrayList
泛型的一些限制
由於型別擦除機制的存在,在使用泛型類和方法時存在一些限制。
1. 泛型類不能使用基本型別的實參進行初始化
2. 在執行時,所有的型別都是原始型別
3. 不能例項化型別變數
4. 不能構造一個泛型型別的陣列
5. 不能在static上下文中使用型別變數(因為靜態域一個類中只有一份)
6. 不能定義擦除後會造成衝突的方法
7. 不能丟擲泛型類物件的異常,也不能使用型別變數來捕獲異常
反射和泛型
Class類
虛擬機器中的泛型資訊
對於Collections類的方法static <T extends Comparable<? super T>> void sort(List<T> list)
來說,可以這樣通過反射得知整個方法簽名。
Method m = Collections.class.getMethod("sort", List.class);
TypeVariable<Method>[] vars = m.getTypeParameters();
String name = vars[0].getName(); // "T"
java.lang.reflect
包中的Type介面用於表示泛型型別宣告。該介面有如下子型別:
1. Class類,用於描述具體的型別
2. TypeVariable介面,描述型別變數(如T extends Comparable<? super T>
)
3. WildcardType介面,描述通配型別(如? super T
)
4. ParameterizedType介面,描述泛型類或者介面型別(如Comparable<? super T>
)
5. GenericArrayType介面,描述泛型陣列(如T[]
)
sort方法中的T
型別變數有一個bound,可以這樣來處理:
Type[] bounds = vars[0].getBounds();
if (bounds[0] instanceof ParameterizedType) { // Comparable<? super T>
ParameterizedType p = (ParameterizedType) bounds[0];
Type[] typeArguments = p.getActualTypeArguments();
if (typeArguments[0] instanceof WildcardType) { // ? super T
WildcardType t = (WildCardType) typeArguments[0];
Type[] upper = t.getUpperBounds(); // ? extends … & …
Type[] lower = t.getLowerBounds(); // ? super … & …
if (lower.length > 0) {
String description = lower[0].getTypeName(); // "T"
…
}
}
}