Java泛型詳解

一、引入泛型機制的原因

假如我們想要實現一個String數組,并且要求它可以動態(tài)改變大小,這時我們都會想到用ArrayList來聚合String對象。然而,過了一陣,我們想要實現一個大小可以改變的Date對象數組,這時我們當然希望能夠重用之前寫過的那個針對String對象的ArrayList實現。

在Java 5之前,ArrayList的實現大致如下:

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

基于繼承的泛型實現會帶來兩個問題:第一個問題是有關get方法的,我們每次調用get方法都會返回一個Object對象,每一次都要強制類型轉換為我們需要的類型,這樣會顯得很麻煩;第二個問題是有關add方法的,假如我們往聚合了String對象的ArrayList中加入一個File對象,編譯器不會產生任何錯誤提示,而這不是我們想要的。

所以,從Java 5開始,ArrayList在使用時可以加上一個類型參數(type parameter),這個類型參數用來指明ArrayList中的元素類型。類型參數的引入解決了以上提到的兩個問題,如以下代碼所示:

ArrayList<String> s = new ArrayList<String>();
s.add("abc");
String s = s.get(0); //無需進行強制轉換
s.add(123);  //編譯錯誤,只能向其中添加String對象
...

在以上代碼中,編譯器“獲知”ArrayList的類型參數String后,便會替我們完成強制類型轉換以及類型檢查的工作。

二、泛型類

public class Box<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

這樣我們的Box類便可以得到復用,我們可以將T替換成任何我們想要的類型:

Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();

三、泛型方法

聲明一個泛型方法很簡單,只要在返回類型前面加上一個類似<K, V>的形式就行了:

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());
    }
}
public 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);

或者在Java1.7/1.8利用type inference,讓Java自動推導出相應的類型參數:

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

四、邊界符

現在我們要實現這樣一個功能,查找一個泛型數組中大于某個特定元素的個數,我們可以這樣實現:

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

但是這樣很明顯是錯誤的,因為除了short, int, double, long, float, byte, char等原始類型,其他的類并不一定能使用操作符>,所以編譯器報錯,那怎么解決這個問題呢?答案是使用邊界符。

public interface Comparable<T> {
    public int compareTo(T o);
}

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

五、通配符(PECS原則)

1 <? extends T>

首先我們先定義幾個簡單的類,下面我們將用到它:

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}

下面這個例子中,我們創(chuàng)建了一個泛型類Reader,然后在f1()中當我們嘗試Fruit f = fruitReader.readExact(apples);編譯器會報錯,因為List<Fruit>與List<Apple>之間并沒有任何的關系。

public class GenericReading {
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruit = Arrays.asList(new Fruit());
    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }
    static void f1() {
        Reader<Fruit> fruitReader = new Reader<Fruit>();
        // Errors: List<Fruit> cannot be applied to List<Apple>.
        // Fruit f = fruitReader.readExact(apples);
    }
    public static void main(String[] args) {
        f1();
    }
}

但是按照我們通常的思維習慣,Apple和Fruit之間肯定是存在聯系,然而編譯器卻無法識別,那怎么在泛型代碼中解決這個問題呢?我們可以通過使用通配符來解決這個問題:

static class CovariantReader<T> {
    T readCovariant(List<? extends T> list) {
        return list.get(0);
    }
}
static void f2() {
    CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
    Fruit f = fruitReader.readCovariant(fruit);
    Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
    f2();
}

這樣就相當與告訴編譯器, fruitReader的readCovariant方法接受的參數只要是滿足Fruit的子類就行(包括Fruit自身),這樣子類和父類之間的關系也就關聯上了。

2 <? super T>

上面我們看到了類似<? extends T>的用法,利用它我們可以從list里面get元素,那么我們可不可以往list里面add元素呢?我們來嘗試一下:

public class GenericsAndCovariance {
    public static void main(String[] args) {
        // Wildcards allow covariance:
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // Compile Error: can't add any type of object:
        // flist.add(new Apple())
        // flist.add(new Orange())
        // flist.add(new Fruit())
        // flist.add(new Object())
        flist.add(null); // Legal but uninteresting
        // We Know that it returns at least Fruit:
        Fruit f = flist.get(0);
    }
}

答案是否定,Java編譯器不允許我們這樣做,為什么呢?對于這個問題我們不妨從編譯器的角度去考慮。因為List<? extends Fruit> flist它自身可以有多種含義:

List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
  • 當我們嘗試add一個Apple的時候,flist可能指向new ArrayList<Orange>();
  • 當我們嘗試add一個Orange的時候,flist可能指向new ArrayList<Apple>();
  • 當我們嘗試add一個Fruit的時候,這個Fruit可以是任何類型的Fruit,而flist可能只想某種特定類型的Fruit,編譯器無法識別所以會報錯。

所以對于實現了<? extends T>的集合類只能將它視為Producer向外提供(get)元素,而不能作為Consumer來對外獲取(add)元素。

如果我們要add元素應該怎么做呢?可以使用<? super T>:

public class GenericWriting {
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();
    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }
    static void f1() {
        writeExact(apples, new Apple());
        writeExact(fruit, new Apple());
    }
    static <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item)
    }
    static void f2() {
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruit, new Apple());
    }
    public static void main(String[] args) {
        f1(); f2();
    }
}

根據上面的例子,我們可以總結出一條規(guī)律,”Producer Extends, Consumer Super”:

  • “Producer Extends” – 如果你需要一個只讀List,用它來produce T,那么使用? extends T。
  • “Consumer Super” – 如果你需要一個只寫List,用它來consume T,那么使用? super T。

如果需要同時讀取以及寫入,那么我們就不能使用通配符了。
如何閱讀過一些Java集合類的源碼,可以發(fā)現通常我們會將兩者結合起來一起用,比如像下面這樣:

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

六、類型擦除

類型擦除就是說Java泛型只能用于在編譯期間的靜態(tài)類型檢查,然后編譯器生成的代碼會擦除相應的類型信息,這樣到了運行期間實際上JVM根本就不知道泛型所代表的具體類型。這樣做的目的是因為Java泛型是1.5之后才被引入的,為了保持向下的兼容性,所以只能做類型擦除來兼容以前的非泛型代碼。對于這一點,如果閱讀Java集合框架的源碼,可以發(fā)現有些類其實并不支持泛型。

我們先來看一下下面這個簡單的例子:

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; }
    // ...
}

編譯器做完相應的類型檢查之后,實際上到了運行期間上面這段代碼實際上將轉換成:

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; }
    // ...
}

由于在虛擬機中泛型類Pair變?yōu)樗膔aw type,因而getData方法返回的是一個Object對象,而從編譯器的角度看,這個方法返回的是我們實例化類時指定的類型參數的對象。實際上,是編譯器幫我們完成了強制類型轉換的工作。也就是說編譯器會把對Node泛型類中getData方法的調用轉化為兩條虛擬機指令:

  • 第一條是對raw type方法getFirst的調用,這個方法返回一個Object對象;
  • 第二條指令把返回的Object對象強制類型轉換為當初我們指定的類型參數類型。

七、泛型注意事項

1 不能用基本類型實例化類型參數

也就是說,以下語句是非法的:

Pair<int, int> pair = new Pair<int, int>();

2 不能拋出也不能捕獲泛型類實例

泛型類擴展Throwable即為不合法,因此無法拋出或捕獲泛型類實例。但在異常聲明中使用類型參數是合法的:

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

3 參數化類型的數組不合法

在虛擬機進行類型擦除后,實際上pairs成為了Pair[]數組,我們可以將它向上轉型為Object[]數組。這時我們若往其中添加Pair<Date, Date>對象,便能通過編譯時檢查和運行時檢查,而我們的本意是只想讓這個數組存儲Pair<String, String>對象,這會產生難以定位的錯誤。因此,Java不允許我們通過以上的語句形式聲明并初始化一個泛型數組。

Pair<String, String>[] pairs = new Pair<String, String>[10];

可用如下語句聲明并初始化一個泛型數組:

Pair<String, String>[] pairs = (Pair<String, String>[]) new Pair[10];

4 不能實例化類型變量

不能以諸如“new T(...)", "new T[...]", "T.class"的形式使用類型變量。Java禁止我們這樣做的原因很簡單,因為存在類型擦除,所以類似于"new T(...)"這樣的語句就會變?yōu)椤眓ew Object(...)", 而這通常不是我們的本意。我們可以用如下語句代替對“new T[...]"的調用:

arrays = (T[]) new Object[N];

5 泛型類的靜態(tài)上下文中不能使用類型變量

因為普通類中可以定義靜態(tài)泛型方法,關于為什么有這樣的規(guī)定,請考慮下面的代碼:

public class People<T> {
    public static T name;
    public static T getName() {
        ...
    }
}

我們知道,在同一時刻,內存中可能存在不只一個People<T>類實例。假設現在內存中存在著一個People<String>對象和People<Integer>對象,而類的靜態(tài)變量與靜態(tài)方法是所有類實例共享的。那么問題來了,name究竟是String類型還是Integer類型呢?基于這個原因,Java中不允許在泛型類的靜態(tài)上下文中使用類型變量。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 泛型 泛型由來 泛型字面意思不知道是什么類型,但又好像什么類型都是??辞懊嬗玫降募隙加蟹盒偷挠白印?以Array...
    向日花開閱讀 2,241評論 2 6
  • 一、泛型簡介 1.引入泛型的目的 了解引入泛型的動機,就先從語法糖開始了解。 語法糖 語法糖(Syntactic ...
    Ruheng閱讀 4,921評論 2 50
  • 2.6 Java泛型詳解 Java泛型是JDK5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type...
    jianhuih閱讀 751評論 0 3
  • 偶爾給網友們畫畫自拍,我也算是一個網絡畫手,一年多前網絡上流行過一陣子畫自拍,還專門有APP為此而生,然而我沒見到...
    我是KiShua閱讀 718評論 1 4
  • Last July, just as Dr. Ted put it, "It has demonstrated t...
    秦之奮斗閱讀 217評論 0 0

友情鏈接更多精彩內容