1. 程式人生 > 實用技巧 >Java泛型學習---第二篇

Java泛型學習---第二篇

泛型學習第一篇

1.泛型之擦拭法

泛型是一種類似”模板程式碼“的技術,不同語言的泛型實現方式不一定相同。

Java語言的泛型實現方式是擦拭法(Type Erasure)。

所謂擦拭法是指,虛擬機器對泛型其實一無所知,所有的工作都是編譯器做的。

例如,我們編寫了一個泛型類Pair<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;
    }
}

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

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使用擦拭法實現泛型,導致了:

  • 編譯器把型別<T>視為Object
  • 編譯器根據<T>實現安全的強制轉型。

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

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的型別自動為我們實行安全地強制轉型。

瞭解了Java泛型的實現方式——擦拭法,我們就知道了Java泛型的侷限:

侷限一:<T>不能是基本型別,例如int,因為實際型別是ObjectObject型別無法持有基本型別:

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

侷限二:無法取得帶泛型的Class。觀察以下程式碼:

public class Main {
    public static void main(String\[\] args) {

    }
}

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

因為TObject,我們對Pair<String>Pair<Integer>型別獲取Class時,獲取到的是同一個Class,也就是Pair類的Class

換句話說,所有泛型例項,無論T的型別是什麼,getClass()返回同一個Class例項,因為編譯後它們全部都是Pair<Object>

侷限三:無法判斷帶泛型的型別:

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

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

侷限四:不能例項化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<String>()和建立new Pair<Integer>()就全部成了Object,顯然編譯器要阻止這種型別不對的程式碼。

要例項化T型別,我們必須藉助額外的Class<T>引數:

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>的例項,所以我們藉助String.class就可以例項化String型別。

不恰當的覆寫方法

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

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

泛型繼承

一個類可以繼承自一個泛型類。例如:父類的型別是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 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的型別系統結構如下:

                      ┌────┐
                      │Type│
                      └────┘
                         ▲
                         │
   ┌────────────┬────────┴─────────┬───────────────┐
   │            │                  │               │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘

小結

Java的泛型是採用擦拭法實現的;

擦拭法決定了泛型<T>

  • 不能是基本型別,例如:int
  • 不能獲取帶泛型型別的Class,例如:Pair<String>.class
  • 不能判斷帶泛型型別的型別,例如:x instanceof Pair<String>
  • 不能例項化T型別,例如:new T()

泛型方法要防止重複定義方法,例如:public boolean equals(T obj)

子類可以獲取父類的泛型型別<T>


2.extends萬用字元

我們前面已經講到了泛型的繼承關係:Pair<Integer>不是Pair<Number>的子類。

假設我們定義了Pair<T>

public class Pair<T> { ... }

然後,我們又針對Pair<Number>型別寫了一個靜態方法,它接收的引數型別是Pair<Number>

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

上述程式碼是可以正常編譯的。使用的時候,我們傳入:

int sum = PairHelper.add(new Pair<Number>(1, 2));

注意:傳入的型別是Pair<Number>,實際引數型別是(Integer, Integer)

既然實際引數是Integer型別,試試傳入Pair<Integer>

public class Main {

}

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

直接執行,會得到一個編譯錯誤:

incompatible types: Pair<Integer> cannot be converted to Pair<Number>

原因很明顯,因為Pair<Integer>不是Pair<Number>的子類,因此,add(Pair<Number>)不接受引數型別Pair<Integer>

但是從add()方法的程式碼可知,傳入Pair<Integer>是完全符合內部程式碼的型別規範,因為語句:

Number first = p.getFirst();
Number last = p.getLast();

實際型別是Integer,引用型別是Number,沒有問題。問題在於方法引數型別定死了只能傳入Pair<Number>

有沒有辦法使得方法引數接受Pair<Integer>?辦法是有的,這就是使用Pair<? extends Number>使得方法接收所有泛型型別為NumberNumber子類的Pair型別。我們把程式碼改寫如下:

public class Main {

}

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>型別等等,因為DoubleBigDecimal都是Number的子類。

如果我們考察對Pair<? extends Number>型別呼叫getFirst()方法,實際的方法簽名變成了:

<? extends Number> getFirst();

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

Number x = p.getFirst();

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

Integer x = p.getFirst();

這是因為實際的返回型別可能是Integer,也可能是Double或者其他型別,編譯器只能確定型別一定是Number的子類(包括Number型別本身),但具體型別無法確定。

我們再來考察一下Pair<T>set方法:

public class Main {

}

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

原因還在於擦拭法。如果我們傳入的pPair<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

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)除外)。

使用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;

因為NumberIntegerDouble都符合<T extends Number>

Number型別將無法通過編譯:

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

因為StringObject都不符合<T extends Number>,因為它們不是Number型別或Number的子類。

小結

使用類似<? extends Number>萬用字元作為方法引數時表示:

  • 方法內部可以呼叫獲取Number引用的方法,例如:Number n = obj.getFirst();

  • 方法內部無法呼叫傳入Number引用的方法(null除外),例如:obj.setFirst(Number n);

即一句話總結:使用extends萬用字元表示可以讀,不能寫。

使用類似<T extends Number>定義泛型類時表示:

  • 泛型型別限定為Number以及Number的子類。

super萬用字元

我們前面已經講到了泛型的繼承關係:Pair<Integer>不是Pair<Number>的子類。

考察下面的set方法:

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

傳入Pair<Integer>是允許的,但是傳入Pair<Number>是不允許的。

extends萬用字元相反,這次,我們希望接受Pair<Integer>型別,以及Pair<Number>Pair<Object>,因為NumberObjectInteger的父類,setFirst(Number)setFirst(Object)實際上允許接受Integer型別。

我們使用super萬用字元來改寫這個方法:

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

注意到Pair<? super Integer>表示,方法引數接受所有泛型型別為IntegerInteger父類的Pair型別。

下面的程式碼可以被正常編譯:

public class Main {

}

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>萬用字元作為方法引數,表示方法內部程式碼對於引數只能寫,不能讀。

對比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除外)。

一個是允許讀不允許寫,另一個是允許寫不允許讀。

先記住上面的結論,我們來看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()方法的定義就完美地展示了extendssuper的意圖:

  • 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);

而這些都是通過superextends萬用字元,並由編譯器強制檢查來實現的。

PECS原則

何時使用extends,何時使用super?為了便於記憶,我們可以用PECS原則:Producer Extends Consumer Super。

即:如果需要返回T,它是生產者(Producer),要使用extends萬用字元;如果需要寫入T,它是消費者(Consumer),要使用super萬用字元。

還是以Collectionscopy()方法為例:

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

需要返回Tsrc是生產者,因此宣告為List<? extends T>,需要寫入Tdest是消費者,因此宣告為List<? super T>

無限定萬用字元

我們已經討論了<? 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 {

}

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<?>的子類,可以安全地向上轉型。

小結

使用類似<? super Integer>萬用字元作為方法引數時表示:

  • 方法內部可以呼叫傳入Integer引用的方法,例如:obj.setFirst(Integer n);

  • 方法內部無法呼叫獲取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super萬用字元表示只能寫不能讀。

使用extendssuper萬用字元要遵循PECS原則。

無限定萬用字元<?>很少使用,可以用<T>替換,同時它是所有<T>型別的超類。