1. 程式人生 > >Java核心技術之泛型

Java核心技術之泛型

  1. 泛型類(generic class)是帶有一個或者多個型別形參(type parameter)的類。
  2. 泛型方法是帶有型別形參的方法。
  3. 可以要求型別形參必須是一個或多個型別的子型別。
  4. 泛型類不是協變的(invariant):當S是T的子型別時,G<S>G<T>沒有任何關係。
  5. 通過使用通配型別形參(wildcards)G<? extends T>或者G<? super T>,使得一個方法可以接受使用T的子類或者超類例項化(instantiation)的泛型型別。
  6. 泛型類和泛型方法在編譯時,其型別形參將被擦除(erase)。
  7. 由於擦除(erasure)機制的存在,泛型的使用有很多限制(restriction)
  8. Class<T>是一個泛型類,這相當有用,因為newInstance方法可以返回T
  9. 即便泛型類和方法在虛擬機器中執行時已被擦除過了,你仍然可以知道它們是如何宣告的。

泛型類

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必須要實現ComparableSerializable介面:

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"
            …
        }
    }
}