【廖雪峯官方網站/Java教程】泛型

泛型是一種“代碼模板”,可以用一套代碼套用各種類型。
在這裏插入圖片描述

1.什麼是泛型

1.1.泛型入門概念介紹

爲了在ArrayList中兼容不同類型等元素,我們必須把ArrayList變成一種模板:ArrayList<T>,代碼如下:

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

T可以是任何class。這樣一來,我們就實現了:編寫一次模版,可以創建任意類型的ArrayList:

// 創建可以存儲String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 創建可以存儲Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 創建可以存儲Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

因此,泛型就是定義一種模板,例如ArrayList<T>,然後在代碼中爲用到的類創建對應的ArrayList<類型>:

ArrayList<String> strList = new ArrayList<String>();

由編譯器針對類型做檢查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

這樣一來,既實現了編寫一次,萬能匹配,又通過編譯器保證了類型安全:這就是泛型。

1.2.向上轉型

在Java標準庫中的ArrayList<T>實現了List<T>接口,它可以向上轉型爲List<T>:

public class ArrayList<T> implements List<T> {
    ...
}
List<String> list = new ArrayList<String>();

即類型ArrayList<T>可以向上轉型爲List<T>(T不能變!)。

2.使用泛型

2.1.用法示例

使用ArrayList時,如果不定義泛型類型時,泛型類型實際上就是Object:

// 編譯器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此時,只能把<T>當作Object使用,沒有發揮泛型的優勢。
當我們定義泛型類型<String>後,List<T>的泛型接口變爲強類型List<String>:

// 無編譯器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 無強制轉型:
String first = list.get(0);
String second = list.get(1);

2.2.泛型接口

除了ArrayList<T>使用了泛型,還可以在接口中使用泛型。例如,Arrays.sort(Object[])可以對任意數組進行排序,但待排序的元素必須實現Comparable<T>這個泛型接口:

public interface Comparable<T> {
    /**
     * 返回-1: 當前實例比參數o小
     * 返回0: 當前實例與參數o相等
     * 返回1: 當前實例比參數o大
     */
    int compareTo(T o);
}

可以直接對String數組進行排序:

// sort
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] ss = new String[] { "Orange", "Apple", "Pear" };
        Arrays.sort(ss);
        System.out.println(Arrays.toString(ss));
    }
}

這是因爲String本身已經實現了Comparable<String>接口。如果換成我們自定義的Person類型試試:

// sort
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);
        System.out.println(Arrays.toString(ps));
    }
}

class Person implements Comparable<Person> {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
    public String toString() {
        return this.name + "," + this.score;
    }
}

運行上述代碼,可以正確實現按name進行排序。

3.編寫泛型

3.1.三步編寫泛型

如果我們確實需要編寫一個泛型類,那麼,應該如何編寫它?
可以按照以下步驟來編寫一個泛型類。首先,按照某種類型,例如:String,來編寫類:

public class Pair {
    private String first;
    private String last;
    public Pair(String first, String last) {
        this.first = first;
        this.last = last;
    }
    public String getFirst() {
        return first;
    }
    public String getLast() {
        return last;
    }
}

然後,標記所有的特定類型,這裏是String,最後,把特定類型String替換爲T,並申明<T>

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

熟練後即可直接從T開始編寫。

3.2.靜態方法

編寫泛型類時,要特別注意,泛型類型<T>不能用於靜態方法。例如:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 對靜態方法使用<T>:
    public static Pair<T> create(T first, T last) {
        return new Pair<T>(first, last);
    }
}

上述代碼會導致編譯錯誤,我們無法在靜態方法create()的方法參數和返回類型上使用泛型類型T。
有些同學在網上搜索發現,可以在static修飾符後面加一個<T>,編譯就能通過:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 可以編譯通過:
    public static <T> Pair<T> create(T first, T last) {
        return new Pair<T>(first, last);
    }
}

但實際上,這個<T>和Pair<T>類型的<T>已經沒有任何關係了。
對於靜態方法,我們可以單獨改寫爲“泛型”方法,只需要使用另一個類型即可。對於上面的create()靜態方法,我們應該把它改爲另一種泛型類型,例如,<K>:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 靜態泛型方法應該使用其他類型區分:
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
}

這樣才能清楚地將靜態方法的泛型類型和實例類型的泛型類型區分開。

3.3.多個泛型類型

泛型還可以定義多種類型。例如,我們希望Pair不總是存儲兩個類型一樣的對象,就可以使用類型<T, K>:

public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public K getLast() { ... }
}

使用的時候,需要指出兩種類型:

Pair<String, Integer> p = new Pair<>("test", 123);

Java標準庫的Map<K, V>就是使用兩種泛型類型的例子。它對Key使用一種類型,對Value使用另一種類型。

4.擦拭法

4.1.擦拭法基本概念

泛型是一種類似"模板代碼"的技術,不同語言的泛型實現方式不一定相同。Java語言的泛型實現方式是擦拭法(Type Erasure)。所謂擦拭法是指,虛擬機對泛型其實一無所知,所有的工作都是編譯器做的
例如,我們編寫了一個泛型類Pair,這是編譯器看到的代碼:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

而虛擬機根本不知道泛型。這是虛擬機執行的代碼:

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}

因此,Java使用擦拭法實現泛型,導致了:

  • 編譯器把類型視爲Object;
  • 編譯器根據實現安全的強制轉型。

使用泛型的時候,我們編寫的代碼也是編譯器看到的代碼:

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虛擬機執行的代碼並沒有泛型:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以,Java的泛型是由編譯器在編譯時實行的,編譯器內部永遠把所有類型T視爲Object處理,但是,在需要轉型的時候,編譯器會根據T的類型自動爲我們實行安全地強制轉型。

4.2.擦拭法侷限

4.2.1.侷限1:<T>不能是基本類型

例如int,因爲實際類型是Object,Object類型無法持有基本類型:

Pair<int> p = new Pair<>(1, 2); // compile error!

4.2.2.侷限2:無法取得帶泛型的Class

觀察以下代碼:

public class Main {
    public static void main(String[] args) {
        Pair<String> p1 = new Pair<>("Hello", "world");
        Pair<Integer> p2 = new Pair<>(123, 456);
        Class c1 = p1.getClass();
        Class c2 = p2.getClass();
        System.out.println(c1 == c2); // true
        System.out.println(c1 == Pair.class); // true

    }
}

class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

因爲T是Object,我們對Pair<String>和Pair<Integer>類型獲取Class時,獲取到的是同一個Class,也就是Pair類的Class。
換句話說,所有泛型實例,無論T的類型是什麼,getClass()返回同一個Class實例,因爲編譯後它們全部都是Pair<Object>。

4.2.3.侷限3:無法判斷帶泛型的Class

Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}

原因和前面一樣,並不存在Pair<String>.class,而是隻有唯一的Pair.class。

4.2.4.侷限4:不能實例化T類型

public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T();
        last = new T();
    }
}

上述代碼無法通過編譯,因爲構造方法的兩行語句:

first = new T();
last = new T();

擦拭後實際上變成了:

first = new Object();
last = new Object();

這樣一來,創建new Pair()和創建new Pair()就全部成了Object,顯然編譯器要阻止這種類型不對的代碼。
要實例化T類型,我們必須藉助額外的Class參數

public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
}

上述代碼藉助Class<T>參數並通過反射來實例化T類型,使用的時候,也必須傳入Class<T>。例如:

Pair<String> pair = new Pair<>(String.class);

因爲傳入了Class的實例,所以我們藉助String.class就可以實例化String類型。

4.3.不恰當的覆寫方法

有些時候,一個看似正確定義的方法會無法通過編譯。例如:

public class Pair<T> {
    public boolean equals(T t) {
        return this == t;
    }
}

這是因爲,定義的equals(T t)方法實際上會被擦拭成equals(Object t),而這個方法是繼承自Object的,編譯器會阻止一個實際上會變成覆寫的泛型方法定義。
換個方法名,避開與Object.equals(Object)的衝突就可以成功編譯:

public class Pair<T> {
    public boolean same(T t) {
        return this == t;
    }
}

4.4.泛型繼承

一個類可以繼承自一個泛型類。例如:父類的類型是Pair<Integer>,子類的類型是IntPair,可以這麼繼承:

public class IntPair extends Pair<Integer> {
}

使用的時候,因爲子類IntPair並沒有泛型類型,所以,正常使用即可:

IntPair ip = new IntPair(1, 2);

前面講了,我們無法獲取Pair<T>的T類型,即給定一個變量Pair<Integer> p,無法從p中獲取到Integer類型。
但是,在父類是泛型類型的情況下,編譯器就必須把類型T(對IntPair來說,也就是Integer類型)保存到子類的class文件中,不然編譯器就不知道IntPair只能存取Integer這種類型。
在繼承了泛型類型的情況下,子類可以獲取父類的泛型類型。例如:IntPair可以獲取到父類的泛型類型Integer。獲取父類的泛型類型代碼比較複雜:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public class Main {
    public static void main(String[] args) {
        Class<IntPair> clazz = IntPair.class;
        Type t = clazz.getGenericSuperclass();
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            Type[] types = pt.getActualTypeArguments(); // 可能有多個泛型類型
            Type firstType = types[0]; // 取第一個泛型類型
            Class<?> typeClass = (Class<?>) firstType;
            System.out.println(typeClass); // Integer
        }

    }
}

class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

class IntPair extends Pair<Integer> {
    public IntPair(Integer first, Integer last) {
        super(first, last);
    }
}

因爲Java引入了泛型,所以,只用Class來標識類型已經不夠了。實際上,Java的類型系統結構如下:
在這裏插入圖片描述

4.5.小結

(1)Java的泛型是採用擦拭法實現的;
(2)擦拭法決定了泛型<T>:

  • 不能是基本類型,例如:int;
  • 不能獲取帶泛型類型的Class,例如:Pair.class;
  • 不能判斷帶泛型類型的類型,例如:x instanceof Pair;
  • 不能實例化T類型,例如:new T()。

(3)泛型方法要防止重複定義方法,例如:public boolean equals(T obj);
(4)子類可以獲取父類的泛型類型。

5.extends通配符

5.1.引入extends通配符

因爲Pair<Integer>不是Pair<Number>的子類,有沒有辦法使得方法參數接受Pair<Integer>?辦法是有的,這就是使用Pair<? extends Number>使得方法接收所有泛型類型爲Number或Number子類的Pair類型。我們把代碼改寫如下:

public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }

    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }

}

class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

這樣一來,給方法傳入Pair<Integer>類型時,它符合參數Pair<? extends Number>類型。這種使用<? extends Number>的泛型定義稱之爲上界通配符(Upper Bounds Wildcards),即把泛型類型T的上界限定在Number了。
除了可以傳入Pair<Integer>類型,我們還可以傳入Pair<Double>類型,Pair<BigDecimal>類型等等,因爲Double和BigDecimal都是Number的子類。
如果我們考察對Pair<? extends Number>類型調用getFirst()方法,實際的方法簽名變成了:

<? extends Number> getFirst();

即返回值是Number或Number的子類,因此,可以安全賦值給Number類型的變量:

Number x = p.getFirst();

然後,我們不可預測實際類型就是Integer,例如,下面的代碼是無法通過編譯的:

Integer x = p.getFirst();

這是因爲實際的返回類型可能是Integer,也可能是Double或者其他類型,編譯器只能確定類型一定是Number的子類(包括Number類型本身),但具體類型無法確定。
我們再來考察一下Pair<T>的set方法:

public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p);
        System.out.println(n);
    }

    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        p.setFirst(new Integer(first.intValue() + 100));
        p.setLast(new Integer(last.intValue() + 100));
        return p.getFirst().intValue() + p.getFirst().intValue();
    }

}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

不出意外,我們會得到一個編譯錯誤:

incompatible types: Integer cannot be converted to CAP#1
where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number

編譯錯誤發生在p.setFirst()傳入的參數是Integer類型。有些童鞋會問了,既然p的定義是Pair<? extends Number>,那麼setFirst(? extends Number)爲什麼不能傳入Integer?
原因還在於擦拭法。如果我們傳入的p是Pair<Double>,顯然它滿足參數定義Pair<? extends Number>,然而,Pair<Double>的setFirst()顯然無法接受Integer類型。
這就是<? extends Number>通配符的一個重要限制:方法參數簽名setFirst(? extends Number)無法傳遞任何Number類型給setFirst(? extends Number)
這裏唯一的例外是可以給方法參數傳入null:

p.setFirst(null); // ok, 但是後面會拋出NullPointerException
p.getFirst().intValue(); // NullPointerException

5.2.extends通配符的作用

如果我們考察Java標準庫的java.util.List<T>接口,它實現的是一個類似“可變數組”的列表,主要功能包括:

public interface List<T> {
    int size(); // 獲取個數
    T get(int index); // 根據索引獲取指定元素
    void add(T t); // 添加一個新元素
    void remove(T t); // 刪除一個已有元素
}

現在,讓我們定義一個方法來處理列表的每個元素:

int sumOfList(List<? extends Integer> list) {
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}

爲什麼我們定義的方法參數類型是List<? extends Integer>而不是List<Integer>?從方法內部代碼看,傳入List<? extends Integer>或者List<Integer>是完全一樣的,但是,注意到List<? extends Integer>的限制:

  • 允許調用get()方法獲取Integer的引用;
  • 不允許調用set(? extends Integer)方法並傳入任何Integer的引用(null除外)。

因此,方法參數類型List<? extends Integer>表明了該方法內部只會讀取List的元素,不會修改List的元素(因爲無法調用add(? extends Integer)、remove(? extends Integer)這些方法。換句話說,這是一個對參數List<? extends Integer>進行只讀的方法(惡意調用set(null)除外)。

5.3.使用extends限定T類型

在定義泛型類型Pair<T>的時候,也可以使用extends通配符來限定T的類型:

public class Pair<T extends Number> { ... }

現在,我們只能定義:

Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;

因爲Number、Integer和Double都符合<T extends Number>。
非Number類型將無法通過編譯:

Pair<String> p1 = null; // compile error!
Pair<Object> p2 = null; // compile error!

因爲String、Object都不符合<T extends Number>,因爲它們不是Number類型或Number的子類。

5.4.小結

使用類似<? extends Number>通配符作爲方法參數時表示:

  • 方法內部可以調用獲取Number引用的方法,例如:Number n = obj.getFirst();;
  • 方法內部無法調用傳入Number引用的方法(null除外),例如:obj.setFirst(Number n);。

即一句話總結:使用extends通配符表示可以讀,不能寫
使用類似<T extends Number>定義泛型類時表示:泛型類型限定爲Number以及Number的子類。

6.super通配符

6.1.super通配符使用舉例

我們使用super通配符來下面的set(*)這個方法:

void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

注意到Pair<? super Integer>表示,方法參數接受所有泛型類型爲Integer或Integer父類的Pair類型。
下面的代碼可以被正常編譯:

public class Main {
    public static void main(String[] args) {
        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);
        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println(p1.getFirst() + ", " + p1.getLast());
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }

    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

考察Pair<? super Integer>的setFirst()方法,它的方法簽名實際上是:

void setFirst(? super Integer);

因此,可以安全地傳入Integer類型。
再考察Pair<? super Integer>的getFirst()方法,它的方法簽名實際上是:

? super Integer getFirst();

這裏注意到我們無法使用Integer類型來接收getFirst()的返回值,即下面的語句將無法通過編譯:

Integer x = p.getFirst();

因爲如果傳入的實際類型是Pair<Number>,編譯器無法將Number類型轉型爲Integer。
注意:雖然Number是一個抽象類,我們無法直接實例化它。但是,即便Number不是抽象類,這裏仍然無法通過編譯。此外,傳入Pair<Object>類型時,編譯器也無法將Object類型轉型爲Integer。
唯一可以接收getFirst()方法返回值的是Object類型:

Object obj = p.getFirst();

因此,使用<? super Integer>通配符表示:

  • 允許調用set(? super Integer)方法傳入Integer的引用;
  • 不允許調用get()方法獲得Integer的引用。

唯一例外是可以獲取Object的引用:Object o = p.getFirst()。
換句話說,使用<? super Integer>通配符作爲方法參數,表示方法內部代碼對於參數只能寫,不能讀

6.2.對比extends和super通配符

我們再回顧一下extends通配符。作爲方法參數,<? extends T>類型和<? super T>類型的區別在於:

  • <? extends T>允許調用讀方法T get()獲取T的引用,但不允許調用寫方法set(T)傳入T的引用(傳入null除外);
  • <? super T>允許調用寫方法set(T)傳入T的引用,但不允許調用讀方法T get()獲取T的引用(獲取Object除外)。

一個(extends)是允許讀不允許寫,另一個(super)是允許寫不允許讀
我們來看Java標準庫的Collections類定義的copy()方法:

public class Collections {
    // 把src的每個元素複製到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

它的作用是把一個List的每個元素依次添加到另一個List中。它的第一個參數是List<? super T>,表示目標List,第二個參數List<? extends T>,表示要複製的List。我們可以簡單地用for循環實現複製。在for循環中,我們可以看到,對於類型<? extends T>的變量src,我們可以安全地獲取類型T的引用,而對於類型<? super T>的變量dest,我們可以安全地傳入T的引用。
這個copy()方法的定義就完美地展示了extends和super的意圖:

  • copy()方法內部不會讀取dest,因爲不能調用dest.get()來獲取T的引用;

  • copy()方法內部也不會修改src,因爲不能調用src.add(T)。

這是由編譯器檢查來實現的。如果在方法代碼中意外修改了src,或者意外讀取了dest,就會導致一個編譯錯誤:

public class Collections {
    // 把src的每個元素複製到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}

這個copy()方法的另一個好處是可以安全地把一個List<Integer>添加到List<Number>,但是無法反過來添加:

// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

而這些都是通過super和extends通配符,並由編譯器強制檢查來實現的。

6.3.PECS原則

何時使用extends,何時使用super?爲了便於記憶,我們可以用PECS原則:Producer Extends Consumer Super。
即:如果需要返回T,它是生產者(Producer),要使用extends通配符;如果需要寫入T,它是消費者(Consumer),要使用super通配符

  • 返回T => 生產者(Producer)=> extends
  • 寫入T => 消費者(Consumer)=> super

還是以Collections的copy()方法爲例:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i); // src是producer
            dest.add(t); // dest是consumer
        }
    }
}

需要返回T的src是生產者,因此聲明爲List<? extends T>,需要寫入T的dest是消費者,因此聲明爲List<? super T>。

6.4.無限定通配符

我們已經討論了<? extends T>和<? super T>作爲方法參數的作用。實際上,Java的泛型還允許使用無限定通配符(Unbounded Wildcard Type),即只定義一個?:

void sample(Pair<?> p) {
}

因爲<?>通配符既沒有extends,也沒有super,因此:

  • 不允許調用set(T)方法並傳入引用(null除外);
  • 不允許調用T get()方法並獲取T引用(只能獲取Object引用)。

換句話說,既不能讀,也不能寫,那隻能做一些null判斷:

static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

大多數情況下,可以引入泛型參數<T>消除<?>通配符

static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}

<?>通配符有一個獨特的特點,就是:Pair<?>是所有Pair<T>的超類

public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        Pair<?> p2 = p; // 安全地向上轉型
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }

}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

上述代碼是可以正常編譯運行的,因爲Pair<Integer>是Pair<?>的子類,可以安全地向上轉型。

6.5.小結

使用類似<? super Integer>通配符作爲方法參數時表示:

  • 方法內部可以調用傳入Integer引用的方法,例如:obj.setFirst(Integer n);
  • 方法內部無法調用獲取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super通配符表示只能寫不能讀;
使用extends和super通配符要遵循PECS原則;
無限定通配符<?>很少使用,可以用<T>替換,同時它是所有<T>類型的超類。

7.泛型和反射(待深入理解)

7.1.使用泛型的反射API

Java的部分反射API也是泛型。

7.1.1.Class<T>就是泛型

// compile warning:
Class clazz = String.class;
String str = (String) clazz.newInstance();

// no warning:
Class<String> clazz = String.class;
String str = clazz.newInstance();

調用Class的getSuperclass()方法返回的Class類型是Class<? super T>:

Class<? super String> sup = String.class.getSuperclass();

7.1.2.構造方法Constructor<T>也是泛型

Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);

7.1.3.可以聲明帶泛型的數組,但不能用new操作符創建帶泛型的數組

Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!

必須通過強制轉型實現帶泛型的數組

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

使用泛型數組要特別小心,因爲數組實際上在運行期沒有泛型,編譯器可以強制檢查變量ps,因爲它的類型是泛型數組。但是,編譯器不會檢查變量arr,因爲它不是泛型數組。因爲這兩個變量實際上指向同一個數組,所以,操作arr可能導致從ps獲取元素時報錯,例如,以下代碼演示了不安全地使用帶泛型的數組:

Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;

ps[0] = new Pair<String>("a", "b");
arr[1] = new Pair<Integer>(1, 2);

// ClassCastException:
Pair<String> p = ps[1];
String s = p.getFirst();

要安全地使用泛型數組,必須扔掉arr的引用:

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

上面的代碼中,由於拿不到原始數組的引用,就只能對泛型數組ps進行操作,這種操作就是安全的。
帶泛型的數組實際上是編譯器的類型擦除:

Pair[] arr = new Pair[2];
Pair<String>[] ps = (Pair<String>[]) arr;

System.out.println(ps.getClass() == Pair[].class); // true

String s1 = (String) arr[0].getFirst();
String s2 = ps[0].getFirst();

所以我們不能直接創建泛型數組T[],因爲擦拭後代碼變爲Object[]:

// compile error:
public class Abc<T> {
    T[] createArray() {
        return new T[5];
    }
}

必須藉助Class<T>來創建泛型數組

T[] createArray(Class<T> cls) {
    return (T[]) Array.newInstance(cls, 5);
}

我們還可以利用可變參數創建泛型數組T[]

public class ArrayHelper {
    @SafeVarargs
    static <T> T[] asArray(T... objs) {
        return objs;
    }
}

String[] ss = ArrayHelper.asArray("a", "b", "c");
Integer[] ns = ArrayHelper.asArray(1, 2, 3);

7.2.謹慎使用泛型可變參數

在上面的例子中,我們看到,通過:

static <T> T[] asArray(T... objs) {
    return objs;
}

似乎可以安全地創建一個泛型數組。但實際上,這種方法非常危險。以下代碼來自《Effective Java》的示例:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] arr = asArray("one", "two", "three");
        System.out.println(Arrays.toString(arr));
        // ClassCastException:
        String[] firstTwo = pickTwo("one", "two", "three");
        System.out.println(Arrays.toString(firstTwo));
    }

    static <K> K[] pickTwo(K k1, K k2, K k3) {
        return asArray(k1, k2);
    }

    static <T> T[] asArray(T... objs) {
        return objs;
    }

}

直接調用asArray(T…)似乎沒有問題,但是在另一個方法中,我們返回一個泛型數組就會產生ClassCastException,原因還是因爲擦拭法,在pickTwo()方法內部,編譯器無法檢測K[]的正確類型,因此返回了Object[]。
如果仔細觀察,可以發現編譯器對所有可變泛型參數都會發出警告,除非確認完全沒有問題,纔可以用@SafeVarargs消除警告。
如果在方法內部創建了泛型數組,最好不要將它返回給外部使用
更詳細的解釋請參考《Effective Java》“Item 32: Combine generics and varargs judiciously”。

7.3.小結

  • 部分反射API是泛型,例如:Class<T>,Constructor<T>;
  • 可以聲明帶泛型的數組,但不能直接創建帶泛型的數組,必須強制轉型;
  • 可以通過Array.newInstance(Class<T>, int)創建T[]數組,需要強制轉型;
  • 同時使用泛型和可變參數時需要特別小心。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章