Java基礎教程(21)--泛型

一.爲什麼使用泛型

  泛型意味着編寫的代碼可以被很多不同類型的對象所重用。例如,我們不希望爲存放String和Integer對象的集合設計不同的類。現在的ArrayList類可以存放任何類型的對象,但是在Java中增加泛型之前已經有了一個ArrayList類,它是使用繼承來實現泛型的。這個ArrayList類只維護一個Object數組:

public class ArrayList {
    private Object [] elementData ;
    public Object get (int i) {...}
    public void add (Object o) {...}
}

  這樣存在兩個問題。第一是可以向集合中添加任何對象:

ArrayList collection = new ArrayList();
collection.add(new Integer(0));
collection.add("string");

  第二是在獲取值時必須進行強制類型轉換:

Integer i0 = (Integer)collection.get(0);

  如果我們默認這個集合是用來存放Integer對象的,那麼下面的類型轉換可以通過編譯,但是在運行時會產生錯誤:

Integer i1 = (Integer)collection.get(1);

  泛型提供了一個更好的解決方案:類型參數。現在的ArrayList類有一個類型參數用來指示元素的類型:

ArrayList<Integer> collection = new ArrayList<Integer>();

  這使得代碼具有更好的可讀性,一看就知道這個集合中存放的是Integer對象。

注:在Java SE 7及以後的版本中,構造函數中可以省略泛型類型。也就是說,上面的語句實際上可以這麼寫:

ArrayList<Integer> collection = new ArrayList<>();

  編譯器也可以很好地利用這個信息。當調用get的時候,不需要進行強制類型轉換,編譯器就知道返回值類型是Integer,而不是Object:

Integer i0 = collection.get(0);

  而且,在插入元素時編譯器會進行檢査,避免插人錯誤類型的對象。例如:

collection.add("string");

  這句代碼是無法通過編譯的。出現編譯錯誤比運行時出現類的強制類型轉換異常要好得多。類型參數的魅力在於可以使程序具有更好的可讀性和安全性。

二.泛型類型

  泛型類型就是將參數類型化的類或接口。就是具有一個或多個類型參數的類。我們先定義一個簡單的Box類,然後使用泛型對它進行改造。

1.一個簡單的Box類

  我們可以將Box類看作是一個盒子,它可以用來存放一個對象。這裏我們使用一個Object類型的成員變量來存放對象,並且需要提供兩個方法:一個是將對象放入盒子的set方法,另一個是獲取盒子中的對象的get方法。

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

  由於它內部維護的是一個Object對象的引用,因此你可以放入任何類型的對象。但是當你取出對象時,你必須進行強制類型轉換。這樣就會存在一個問題,如果你存入的對象類型與你轉換的類型不一致,將會在運行時出現異常,但是編譯器卻無法在編譯時期發現這個問題。例如:

Box b = new Box();
b.set("box");
Integer i = (Integer) b.get();

  上面的代碼可以通過編譯,但是會在運行的時候拋出一個ClassCastException。

2.使用泛型的Box類

  泛型類使用如下的語法定義:

class ClassName<T1, T2, ..., Tn> {...}

  類名後面是類型參數(也稱類型變量),使用一對尖括號<>括起來,一個類中可以有多個類型參數。
  下面是使用泛型改造後的Box類:

public class Box<T> {
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

  所有的Object都被替換成了類型變量T。在使用時,類型變量可以是任何非基本數據類型。
  泛型接口的語法與泛型類類似:

interface InterfaceName<T1, T2, ..., Tn> {...}

3.類型參數的命名慣例

  按照慣例,類型參數名稱是單個大寫字母。這與我們已經瞭解的變量命名規範有很大的出入,但是這有充分的理由:如果沒有這種約定,就很難區分類型變量和類或接口的名稱。
  下面是最常用的類型參數的名稱:

  • E - 元素(廣泛應用於集合中)
  • K - 鍵
  • V - 值
  • N - 數字
  • T - 類型
  • S,U,V等 - 第二、第三、第四類型

4.調用和實例化泛型類型

  要在代碼中使用Box類,必須執行泛型類型調用,也就是將T替換爲具體的類型,比如Integer:

Box<Integer> integerBox;

  你可以把泛型類型調用看作是普通的方法調用,但是不同於將參數傳遞給方法,泛型類型調用將類型參數(在這個例子中是Integer)傳遞給泛型類。
  正如其他的變量聲明一樣,上面的代碼並沒有創建Box對象,它只是聲明瞭一個可以引用裝有Integer對象的Box對象的引用。
  泛型類型調用也被稱作是類型參數化。
  要實例化這個類,可以像實例化其他類一樣使用new關鍵字,但是要在類名和小括號之間加上<Integer>:

Box<Integer> integerBox = new Box<Integer>();

5.鑽石運算符

  在Java SE 7及以後的版本中,在調用泛型類的構造方法時可以省略尖括號中的類型參數,這對尖括號被稱爲鑽石運算符。例如,你可以使用下面的語句來實例化Box<Integer>:

Box<Integer> integerBox = new Box<>();

6.原始類型

  原始類型是指沒有類型參數的泛型類或接口。例如下面的泛型類Box:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

  要創建一個類型參數化的Box,需要爲類型參數T提供一個實際的類型:

Box<Integer> intBox = new Box<>();

  如果沒有提供實際的類型,將會創建一個Box的原始類型:

Box rawBox = new Box();

  因此,Box就是Box的原始類型。但是,非泛型類或接口不是原始類型。
  原始類型多出現在遺留代碼中,這是因爲很多類或接口(例如集合類)在JDK 5.0之前不是泛型的。在使用原始類型時,你實際上已經獲得了預泛型行爲。爲了向後兼容,允許將一個參數化的類型賦值給原始類型:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;           // OK

  但是如果你把原始類型賦值給參數化的類型,就會產生警告:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

  如果使用原始類型調用泛型類型中定義的使用了泛型的方法,也會產生警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);                    // warning: unchecked invocation to set(T)

  警告顯示原始類型將會繞過泛型類型檢查,將不安全代碼的捕獲推遲到運行時。因此,應該避免使用原始類型。

三.泛型方法

  泛型方法是指引入了自己獨有的類型參數的方法。這類似於聲明泛型類型,但類型參數的使用範圍僅限於聲明它的方法。允許使用靜態和非靜態泛型方法,以及泛型構造方法。
  定義泛型方法時,要將使用尖括號包圍的類型參數列表放在修飾符之後,返回值之前。
  下面的Util類中定義了一個泛型方法compare,這個方法可以用來比較兩個Pair對象:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
    }
}

class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

  調用該方法的語法如下:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

  上面的代碼中顯式提供了類型參數。一般來說,可以省略類型參數,編譯器將會自動推斷出需要的類型。

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

  這個特性被稱爲類型推斷,允許將泛型方法作爲普通方法調用,而無需在尖括號之間指定類型。我們稍後將會進一步討論有關類型推斷的內容。
  構造方法也可以是泛型方法,並且非泛型類也可以擁有泛型構造方法,例如:

class MyClass {
    <T> MyClass(T t) {
        ...
    }
}

四.有界類型參數

  有時你可能希望限制類型參數的類型。例如,對數字進行操作的方法可能只想接受Number及其子類的實例。這正是有界類型參數的用途。
  要聲明有界類型參數,需要在類型參數後面使用extends關鍵字,然後跟上這個類型參數的上界。需要注意的是,在這種情況下,extends既可以用於表示繼承(對於類而言),又可以用於表示實現(對於接口而言)。

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: argument must be Number or its subclass
    }
}

  上面的程序將會編譯失敗,因爲inspect方法只接受Number類及它的子類的實例作爲參數。
  一個類型參數可以有多個上界:

<T extends B1 & B2 & B3>

  如果一個類型參數有多個上界,那麼這些上界中最多只能有一個類,並且這個類必須是列表中的第一個:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

  如果A沒有放在第一個,將會產生一個編譯錯誤。

五.泛型,繼承和子類型

  正如我們所知,可以將子類的實例賦值給父類類型的變量。例如,可以將Integer類的實例賦值給Number類型的變量,因爲Integer是Number的子類。

Number someNumber = new Integer(10);

  在泛型中也是如此,例如:

Box<Number> box = new Box<Number>();
box.set(new Integer(10));

  現在考慮以下方法:

public void boxTest(Box<Number> n) { /* ... */ }

  這個方法接受一個Box類型的參數。我們是否可以傳遞Box或Box類型的參數呢?答案是否定的。因爲Box和Box不是Box的子類。在使用泛型編程時,這是一個常見的誤解,但這是一個需要學習的重要概念。

  給定兩個具體類型A和B(例如,Number和Integer),MyClass<A>與MyClass<B>無關,無論A和B是否相關。MyClass<A>和MyClass<B>的公共父類是Object。
  兩個泛型類型的關係是通過它們之間的繼承(或實現)語句確定的,而不是通過類型參數之間的關係確定的。只要兩個泛型類型之間存在繼承(或實現)關係,並且不改變類型參數,就會在類型之間保留子類型關係。
  使用ArrayList類作爲示例。ArrayList實現了List接口,而List接口又繼承了Collection接口。所以ArrayList是List和Collection的子類型,List是Collection的子類型。只要不改變參數類型,他們之間就存在子類型關係:

  現在假設我們要定義一個自己的接口PayLoadList,它繼承自List接口,同時引入了新的類型參數P:

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

  只要我們保證傳遞給類型參數E的類型和List一致,無論傳遞給P的類型是什麼,這個接口都是List的子類型,例如:

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

六.類型推斷

  類型推斷是Java編譯器的一種根據方法調用和對應的聲明來推斷類型參數的具體類型的能力。類型推斷試圖找到適用於所有參數的最貼切的類型。例如:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

  在上面的例子中,T將會被推斷爲Serializable類型。因爲a1是String類型,a2是ArrayList類型,因此編譯器將會尋找它們的共同超類型,也就是Serializable類型。

1.類型推斷和泛型方法

  在泛型方法中我們已經提到了類型推斷,它使我們能夠像調用普通方法一樣調用泛型方法,而無需在尖括號之間指定類型。考慮下面的例子:

public class BoxDemo {
    public static <U> void addBox(U u, List<Box<U>> boxes) {
        Box<U> box = new Box<>();
        box.set(u);
        boxes.add(box);
    }
    public static <U> void outputBoxes(List<Box<U>> boxes) {
        int counter = 0;
        for (Box<U> box: boxes) {
            U boxContents = box.get();
            System.out.println("Box #" + counter + " contains [" + boxContents.toString() + "]");
            counter++;
        }
    }
    public static void main(String[] args) {
        ArrayList<Box<Integer>> listOfIntegerBoxes = new ArrayList<>();
        BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes); // statement 1
        BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);          // statement 2
        BoxDemo.outputBoxes(listOfIntegerBoxes);
    }
}

  上面的例子輸出如下:

Box #0 contains [10]
Box #1 contains [20]

  泛型方法addBox定義了一個類型參數U。當調用addBox時,在方法前面的尖括號中放入具體的類型,正如上面的statement 1:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

  不過,大多數情況下,編譯器可以根據方法調用來推斷出類型參數的具體類型,因此可以省略方法名後面的尖括號和類型,正如上面的statement 2:

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

2.類型推斷和構造方法

  在泛型類和非泛型類中,都可以包含泛型構造方法(換句話說就是可以引入它們自己的類型參數)。首先我們看一個泛型類的例子:

class MyClass<X> {
    <T> MyClass(T t) {
        ...
    }
}

  首先需要明確一點的是,在實例化一個泛型類時,構造方法後面的尖括號裏的參數列表與聲明泛型類時類名後的尖括號裏的參數列表是一一對應的,也就是說,無論構造方法是不是泛型方法,它都是提供給泛型類引入的類型參數使用的。這與普通的泛型方法不一樣,普通的泛型方法在調用時尖括號中的參數是提供給該方法使用的。這就意味着,如果一個泛型類包含泛型構造方法,那麼在實例化該泛型類時,無論構造方法後的尖括號中是否省略了類型參數列表,該構造方法始終需要推斷出自己引入的類型參數的具體類型。因此,可以像下面這樣實例化上面的泛型類:

MyClass<Integer> myObject1 = new MyClass<Integer>("obj1"); // statement 1
MyClass<Integer> myObject2 = new MyClass<>("obj2");        // statement 2

  上面的statement 1中,構造方法後面提供了X的類型,因此無需推斷,但由於無法提供T的類型,因此編譯器需要從構造方法的參數中推斷出其類型String;在statement 2中,X和T的類型都沒有提供,因此均需要推斷。
  下面再來看一個非泛型類的例子:

class MyClass {
    <T> MyClass(T t) {
        ...
    }
}

  因爲這是一個非泛型類,在實例化時構造方法後面不能使用尖括號,因此仍然無法提供該泛型構造方法需要的類型信息,那麼T的類型必須通過推斷得出:

MyClass myObject = new MyClass("obj");

  經過類型推斷可以得出T的類型是String。

七.通配符

  在泛型程序設計中,問號(?)被稱爲通配符,它表示未知的類型。通配符可以被用在很多場景中:參數、域或局部變量的類型,有時候也用在返回值類型中(不過更好的編程實踐是使用具體的返回值類型)。下面將更詳細地討論通配符。

1.上界通配符

  可以使用上界通配符來放寬對變量的限制。例如,如果你的方法接受的參數是一個List,它裏面的元素可以是Number以及它的子類的實例,你可以使用上界通配符來實現這一點。
  要聲明一個上界通配符,在尖括號中使用?,後面跟上extends關鍵字和它的上界。在這種情況下,extends既可以用於表示繼承(對於類而言),又可以用於表示實現(對於接口而言)。
  要編寫適用於Number及其子類類型的集合的方法,你可以指明List<? extends Number>。List<Number>比List<? extends Number>更嚴格,因爲前者只接受Number類型的集合,而後者可以接受Number及其子類類型的集合。
  下面的sumOfList方法返回一個集合中的數字的總和:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

  以下的代碼使用了一個Integer集合,將會輸出sum = 6.0:

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

  下面的代碼使用了Double集合,將會輸出sum = 7.0:

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

2.無界通配符

  無界通配符只用到了問號(?),例如List<?>。在以下兩種場景中無界通配符很有用:

  • 對於通配符表示的類型,你編寫的方法只用到了來自Object類的方法。
  • 你的代碼只使用到了泛型類中不依賴於類型參數的方法,例如List的size()和clear()方法。

  考慮以下方法:

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

  printList的目標是打印任何類型的集合,但它無法實現該目標,因爲它只能打印Object類型的集合。它不能打印List<Integer>,List<String>,List<Double>等,因爲它們不是List<Object>的子類型。要編寫通用的printList方法,需要使用List<?>:

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

  對於任意的具體類型A,List<A>是List<?>的子類型,可以使用printList打印任何類型的集合:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

  要注意的是,List<Object>和List<?>並不一樣。List<Object>中可以插入任意類型的對象,而List<?>中只能插入null。

3.下界通配符

  與上界通配符類似,下界通配符將類型參數限制爲某個特定類型及它的超類型。不過需要注意的是,不能同時指定通配符的上界和下界。要聲明一個下界通配符,在尖括號中使用?,後面跟上super關鍵字和它的下界。
  假設我們要編寫一個將Integer對象放入集合的方法。爲了最大限度地提高靈活性,我們希望該方法可以處理List<Integer>,List<Number>和List<Object>,也就是任何可以保存Integer值的集合。
  要編寫適用於Integer及其超類型的集合的方法,你可以指明List<? super Integer>。List<Integer>比List<? super Integer>更嚴格,因爲前者只接受Integer類型的集合,而後者可以接受Integer及其超類型的集合。
  下面的代碼將數字1~10添加到指定的集合中:

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

3.通配符和子類型

  正如第五小節《泛型,繼承和子類型》中描述的那樣,泛型類或接口之間的關係與類型參數並沒有直接聯繫。然而,可以使用通配符在泛型類或接口之間創建關係。
  觀察以下代碼:

Integer i = new Integer(1);
Number n = i;

  上面的代碼是合理的。這個例子展示了常規類的繼承規則:如果類B繼承了類A,則類B是類A的子類型。但是這條規則在泛型類型中並不成立:

List<Integer> li = new ArrayList<>();
List<Number> ln = li;   // compile-time error

  Integer是Number的子類型,那麼List<Integer>和List<Number>之間到底是什麼關係?

  儘管Integer是Number的子類型,但List<Integer>不是List<Number>的子類型。List<Integer>和List<Number>的公共超類型是List<?>。
  如果不好理解,可以回想一下面向對象程序設計中對於繼承的定義。在面向對象程序設計中,繼承是通過is-a來體現的。可以這麼說,如果B is a A,那麼A就是B的超類型。例如,任何對象都是一個Object,因此Object是所有類的超類型。
  現在我們回過頭來看List<Integer>和List<Number>,List<Integer>表示一個Integer類型的集合,List<Number>是一個Number類型的集合,這兩個集合中的元素不是同一種類型。因此,儘管Number是Integer的超類型,但卻不能說一個Integer類型的集合是一個Number類型的集合,也就是說List<Number>不是List<Integer>的超類型。
  再來說說這兩個集合和List<?>。List<?>表示一個任何類型元素的集合,既然可以表示任何元素的集合,自然也就可以表示Integer類型的集合和Number類型的集合。下面的代碼是合理的:

List<Integer> li = new ArrayList<>();
List<Number> ln = new ArrayList<>();
List<?> l1 = li;
List<?> l2 = ln;

  在理解了這個概念之後,我們就可以判斷使用通配符的泛型類型之間的關係了。下面的圖片展示了幾個使用通配符的List之間的關係,大家可以試着自己理解:

4.通配符捕獲

  下面的rebox方法將元素從Box中取出並重新放回:

public class Box<T> {
    private T value;

    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }

    public static void rebox(Box<?> box) {
        box.set(box.get());
    }
}

  這段代碼看上去應該可以運行,因爲取出的值和放回的值是相同類型的。但是,在編譯時卻會產生以下錯誤信息:

  在我們討論這個錯誤之前,我們首先來區分一下通配符(?)和類型參數(以下使用T來說明)的區別。雖然它們都用來表示未知的類型,但是類型參數T會在第一次用到它的時候就確定下來,之後程序中的所有類型參數T都代表這個類型;而通配符?則不同,程序中出現的每個通配符都會獲得不同的捕獲,編譯器會爲每個通配符的捕獲分配不同的名稱,因爲任意未知的類型參數之間並沒有關係。
  現在上面出現的錯誤提示就很容易理解了。rebox方法的box參數是Box<?>類型的,那麼編譯器會將它的set方法的參數標記爲CAP#1,將它的get方法的返回值標記爲CAP#2,由於編譯器並不知道CAP#1與CAP#2是否相同(雖然在這裏它們就是相同的),因此就會出現上面的錯誤信息。
  那麼是不是rebox方法的功能就無法實現了呢?實際上,這裏我們可以藉助一個輔助方法:

public static void rebox(Box<?> box) {
    reboxHelper(box);
}

private static <T> void reboxHelper(Box<T> box) {
    box.set(box.get());
}

  現在rebox方法什麼也沒做,只是將box參數原封不動地傳遞給reboxHelper方法。當reboxHelper方法接收到這個參數的時候,類型參數T就會捕獲通配符所代表的類型,之後的所有T都代表了這個類型,也就不會出現類型轉換所帶來的問題。
  當然這裏的例子只是爲了演示通配符捕獲這個概念,並沒有什麼意義。實際上可以直接將rebox定義爲泛型方法:

public static <T> void rebox(Box<T> box) {
    box.set(box.get());
}

5.通配符使用指南

  在使用泛型進行編程時,令人困惑的一個方面是確定何時使用通配符以及使用哪種類型的通配符。下面提供了幾條編碼時要遵循的一些指導原則。
  首先我們應該考慮該使用泛型的變量屬於哪種類型:

  • in變量:“in”變量將數據提供給代碼。假設有一個帶兩個參數的的複製方法copy(src,dest),src參數提供需要被複制的數據,因此它是“in”變量。
  • out變量:“out”變量用於保存其他地方的數據。在複製方法copy(src,dest)中,dest參數接受數據,所以它是“out”變量。

  當然,一些變量既屬於“in”變量也屬於“out”變量,這種情況在下面的準則中也有說明。
  下面是幾條選擇通配符類型時的準則:

  • “in”變量使用上界通配符
  • “out”變量使用下界通配符
  • 在只需要使用Object類中定義的方法訪問“in”變量的情況下,使用無界通配符
  • 若該變量既屬於“in”變量,又屬於“out”變量,不使用通配符

  不過,這些準則並不適用於方法的返回類型。應避免使用通配符作爲返回類型,因爲它強制程序員使用代碼來處理通配符。

八.類型擦除

  Java中引入泛型的目的是提供更嚴格的編譯時類型檢查,以及更好地支持泛型程序設計。但是對於虛擬機而言,並不存在泛型,所有的類都是普通類。在編譯時,編譯器會將類型參數擦除,並替換爲它的限定類型或Object。類型擦除確保不爲參數化類型創建新的類,因此泛型並不會產生運行時開銷。

1.泛型類型的擦除

  考慮下面單鏈表中的節點類:

public class Node<T> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

  因爲類型參數T沒有限制,所以Java編譯器會將其替換爲Object。也就是說,編譯後的Node類應該是下面這樣的:

public class Node {
    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

  在下面的例子中,Node類使用了有界類型參數:

public class Node<T extends Comparable<T>> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

  Java編譯器會將有界類型參數T替換爲它的限定類型Comparable:

public class Node {
    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

2.泛型方法的擦除

  Java編譯器也會擦除泛型方法中的類型參數。考慮以下的泛型方法,這個方法用來統計一個元素在數組中的出現次數:

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray) {
        if (e.equals(elem)) {
            ++cnt;
        }   
    }
    return cnt; 
}

  因爲類型參數T沒有限制,所以Java編譯器會將其替換爲Object:

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray) {
        if (e.equals(elem)) {
            ++cnt;
        }   
    }
    return cnt; 
}

  假設有以下的幾個類:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

  你可以編寫一個泛型方法來繪製不同的圖形:

public static <T extends Shape> void draw(T shape) { /* ... */ }

  這個方法將會被編譯爲:

public static void draw(Shape shape) { /* ... */ }

3.橋接方法

  考慮下面的Node類和MyNode類:

public class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

  MyNode類繼承了參數化的Node類,也就是Node<Integer>。那麼,擦除類型之後的Node類和MyNode類應該是:

public class Node {
    public Object data;
    public Node(Object data) { this.data = data; }
    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

  MyNode類重寫了Node<Integer>的setData方法,但經過編譯器的類型擦除後,Node<Integer>類並不存在,MyNode繼承的類也變成了Node類。此時,MyNode類的setData方法和Node類的setData方法的參數列表並不相同,並不符合重寫的定義。也就是說,我們在編寫代碼時設計的重寫經過編譯器的類型擦除後就消失了。爲了保持重寫的語義和保留多態性,編譯器會自動生成一個橋接方法。對於MyNode類來說,編譯器將會爲setData生成下面的橋接方法:

public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }

    // Bridge method generated by the compiler
    public void setData(Object data) {
        setData((Integer) data);
    }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

  泛型接口也是如此。考慮下面的泛型接口和實現類:

public interface DemoInterface<T> {
    void foo(T t);
}

public class DemoInterfaceImpl implements DemoInterface<String> {
    void foo(String s) {
        // ...
    }
}

  經過類型擦除後,DemoInterface和DemoInterfaceImpl將會變成:

public interface DemoInterface {
    void foo(Object t);
}

public class DemoInterfaceImpl implements DemoInterface {
    void foo(String s) {
        // ...
    }
}

  我們知道,實現類必須重寫接口中所有的抽象方法。但是DemoInterfaceImpl中的foo(String s)方法並沒有重寫DemoInterface中的foo(Object t)方法,這顯然違背了繼承的原則。此時,編譯器也會在編譯時自動生成橋接方法:

public class DemoInterfaceImpl implements DemoInterface {
    public void foo(Object t) {
        foo((String) t);
    }
    public void foo(String s) {
        // ...
    }
}

4.不可具體化類型

  可具體化類型是指類型信息在運行時完全可用的類型。這包括基本類型,非泛型類型,原始類型和使用了無界通配符的類型。
  不可具體化類型是指那些類型信息在編譯期被擦除的類型,也就是沒有使用無界通配符定義的泛型類型。我們無法在運行時獲取一個不可具體化類型的所有信息。例如List<Number>和List<String>,JVM在運行時無法區分它們。

1.堆污染

  當一個參數化類型的變量引用了一個對象,而這個對象的類型並不是該參數化類型時,就會產生堆污染。如果一個程序執行了一些在編譯時會出現非受檢警告的操作時,就會出現這種情況。非受檢警告既有可能出現在編譯時(在編譯時類型檢查規則的限制下),也有可能出現在運行時無法保證涉及參數化類型操作(例如類型轉換或方法調用)的正確性。例如,將原始類型和參數化類型混合使用時,或者執行了一個未受檢的類型轉換時,堆污染就會發生。
  在正常情況下,當所有代碼同時被編譯後,編譯器就會對潛在的堆污染髮出一個警告以引起你的關注。如果你分模塊對代碼進行編譯,那就很難檢測出潛在的堆污染。如果你確定代碼編譯後沒有產生警告,那麼堆污染就不會發生。

2.具有不可具體化形參的可變參數方法的潛在隱患

  具有可變參數的泛型方法可以引起堆污染。考慮下面的ArrayBuilder類:

public class ArrayBuilder {
    public static <T> void addToList (List<T> listArg, T... elements) {
        for (T x : elements) {
            listArg.add(x);
        }
    }
    public static void faultyMethod(List<String>... l) {
        Object[] objectArray = l;
        objectArray[0] = Arrays.asList(42);
        String s = l[0].get(0);
    }
}

  下面的HeapPollutionExample使用了ArrayBuiler類:

public class HeapPollutionExample {
    public static void main(String[] args) {
        List<String> stringListA = new ArrayList<String>();
        List<String> stringListB = new ArrayList<String>();

        ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
        ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
        List<List<String>> listOfStringLists = new ArrayList<List<String>>();
        ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);

        ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
    }
}

  編譯時,ArrayBuilder.addToList方法會產生這樣的警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

  當編譯遇到可變參數方法時,它會將可變參數翻譯成一個數組。在方法ArrayBuilder.addList中,編譯器將可變形參T... elements翻譯成T[] elements。然而,由於類型擦除,編譯器又將T[] elements轉化爲Object[] elements。這樣就存在堆污染的可能性。
  下面的語句把可變形參l賦值給Object數組objectArgs:

Object[] objectArray = l;

  這個語句也會潛在地引起堆污染。一個不匹配可變形參l的參數化類型的值可以被賦值給變量l,也就可以賦值給objectArray。然而,編譯器並不會在這一句上生成未受檢警告。在將可變形參List<String> l翻譯成形參List[] l時,編譯就已經生成了一個警告。這一句是有效的,因爲變量l是List[]類型,它同時也是Object[]的子類。
  因此,如果你像如下語句那樣,把一個任何類型的List對象賦值給objectArray數組的任何一個元素時,編譯器也不會報任何警告或錯誤。

objectArray[0] = Arrays.asList(42);

  這個語句將objectArray的第一個元素賦值爲一個List對象,這個List持有一個Integer元素。
  當你通過下面的語句去調用ArrayBuilder.faultyMetho方法時,就會拋出一個ClassCastException異常:

String s = l[0].get(0);

  l數組中的第一個元素存儲了一個List類型的對象,但是這一句期望的類型卻是List

3.避免來自不可具體化形參的可變參數方法的警告

  如果你定義了一個具有參數化類型參數的可變參數方法,並且確保你的方法不會拋出一個ClassCastException異常或其他因對可變形參處理不當而引起的相似異常時,你就可以通過在靜態或非構造方法上使用@SafeVarargs註解來人爲地屏蔽編譯器生成的這些警告。這個註釋斷言此方法的實現會合理地處理可變形參。
  如果你還想同時屏蔽非受檢警告,可以像下面這樣:

@SuppressWarnings({"unchecked", "varargs"})

九.泛型的限制

  爲了更有效地使用泛型,你必須瞭解下面這些限制:

1.不能使用基本類型實例化泛型類型

  考慮下面的參數化類型:

class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    // ...
}

  在創建Pair對象的時候,不能使用基本類型來替換K或V:

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

  不過可以使用它們的包裝類來代替,編譯器會對它們進行自動裝箱:

Pair<Integer, Character> p = new Pair<>(8, 'a');

2.不能創建類型參數的實例

  不能創建類型參數的實例。例如,下面的代碼將會導致一個編譯錯誤:

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

3.不能使用類型參數聲明靜態域

  因爲靜態域在類加載時就已經創建,而類型參數在實例化類時纔會指定。因此不能使用類型參數聲明靜態域。下面的代碼也會導致編譯錯誤:

public class MobileDevice<T> {
    private static T os;
    // ...
}

4.不能對類型參數使用類型轉換或instanceof

  因爲編譯器擦除了所有的類型參數,因此你不能在運行時判斷泛型類型使用的是什麼類型參數:

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

  運行時並不能區分ArrayList<Integer>和ArrayList<String>。你能做的就是使用無界通配符來判斷list是不是一個ArrayList:

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

  不能對類型參數進行類型轉換,除非這個類型參數是無界通配符。例如:

List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error

5.不能創建類型參數的數組

  不能實例化參數化類型的數組,例如:

Pair<String>[] table = new Pair<String>[10]; // Error

  這又什麼問題呢?擦除之後,table的類型是Pair[]。可以把它轉換成Object[]:

Object[] objarray = table;

  數組會記住它的元素類型,如果試圖存儲其他類型的元素,就會拋出一個ArrayStoreException異常:

objarray[0] = "Hello";  // Error--component type is Pair

  不過對於泛型類型,擦除會使這種機制失效。以下賦值:

objarray[0] = new Pair<Integer>();

  能夠通過數組存儲檢查,但仍然會導致一個錯誤。出於這個原因,不允許創建參數化類型的數組。
  需要說明的是,只是不允許創建這些數組,而聲明類型爲Pair<String>[]的變量仍是合法的。不過不能用new Pair<String>[10]初始化這個變量。

6.不能拋出或捕獲泛型類的實例

  既不能拋出也不能捕獲泛型類對象。實際上,甚至泛型類擴展Throwable都是不合法的。例如,以下定義就不能正常編譯:

public class Problem<T>extends Exception { ... }  // Error--can't extend Throwable

  不過,在異常規範中使用類型變量是允許的。以下方法是合法的:

public static <T extends Throwable> void doWork(T t) throws T { // OK
    try {
        // ...
    } cathe (Throwable realCause) {
        t.initCause(realCause);
        throw t;
    }
}

7.不能重載擦除類型後參數列表相同的方法

  一個類不能有兩個在類型擦除後具有相同簽名的方法:

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章