天天看點

你真的懂Java泛型嗎

泛型實作參數化類型的概念,使代碼可以應用于多種類型,解除類或方法與所使用的類型之間的限制。在JDK 1.5開始引入了泛型,但Java實作泛型的方式與C++或C#差異很大。在平常寫代碼用到泛型時,仿佛一切都來得如此理所當然。但其實Java泛型還是有挺多tricky的東西的,編譯器在背後為我們做了很多事。下面我們來看看有關Java泛型容易忽視的點。

泛型不支援協變

什麼是協變?舉個例子。

class Fruit{}
class Apple extends Fruit{}
Fruit[] fruit = new Apple[10]; // OK123123      

子類數組可以賦給父類數組的引用。但泛型是不支援這種協變的。

ArrayList<Fruit> flist = new ArrayList<Apple>(); // 無法通過編譯11      

但我們可以使用通配符來解決

ArrayList<? extends Fruit> flist = new ArrayList<Apple>();// 使用通配符解決協變問題11      

通配符

上界通配符

        List<? extends Fruit> flist = Arrays.asList(new Apple());
        Apple a = (Apple)flist.get(0); // No warning
        flist.contains(new Apple()); // Argument is ‘Object’
        flist.indexOf(new Apple()); // Argument is ‘Object’
        //flist.add(new Apple());   無法編譯1234512345      

List<? extends Fruit>

 表示某種特定類型 ( Fruit 或者其子類 ) 的 List,但是編譯器并不關心(不知道)這個實際的具體類型到底是什麼。值得注意的是,這并不意味着這個List可以持有Fruit的任意類型! 

由于List的具體類型是并不确定的,而且Java泛型是不支援協變的,是以帶有泛型類型參數的方法都無法正常調用。比如

add(T item);

,即使是傳Object也無法通過編譯。 

但對于傳回類型是泛型的方法,比如

T get(int index);

,傳回值類型與上界類型一樣。如上面示例代碼調用的

flist.get(0)

傳回值就是Fruit類型的。

下界通配符

    static void add(List<? super Apple> list) {
//        list.add(new Fruit()); // 無法編譯
        Object object = list.get(0);// pass
    }12341234      

代碼中的 

List<? super Apple> list

表明list持有的類型是Apple的父類類型,但與上界通配符類似,這并不意味list可以持有Apple任意的子類類型的對象,編譯器并不知道list具體的類型是什麼。是以,

list.add(new Fruit());

就不能編譯了。

×××通配符

List<?> list

 表示 

list

 是持有某種特定類型的 List,但是不知道具體是哪種類型。而單獨的 

List list

 ,也就是沒有傳入泛型參數,表示這個 list 持有的元素的類型是 

Object

所有泛型資訊都被擦除了嗎

所謂的擦除,僅僅是對方法的Code屬性中的位元組碼(也就是方法内的邏輯代碼)進行擦除,實際上中繼資料(類和接口的聲明,類字段的聲明)中還是保留了泛型資訊。 

引用R大的話就是: 

位于聲明一側的,源碼裡寫了什麼到運作時就能看到什麼; 

位于使用一側的,源碼裡寫什麼到運作時都沒了。

public class GenericClass<T> {                // 1  
    private List<T> list;                     // 2  
    private Map<String, T> map;               // 3  
    public <U> U genericMethod(Map<T, U> m) { // 4  
        List<String> list = new ArrayList<>(); // 5
        return null;
    }
}  1234567812345678      

上面的代碼中,注釋1到注釋4的T和U是保留在Class檔案當中的,源碼是什麼,那麼通過反射擷取得到的就是什麼。也就是說,在運作時,是無法擷取到具體的T和U是什麼類型的。

但運作時,在方法内部的局部變量的泛型資訊是被全部擦除的。如上的注釋5中的list的具體類型是無法在運作時擷取到的。

真的無法擷取到泛型類型嗎

當時今日頭條的面試官問過我這個問題,我當時對泛型的認識比較淺薄,以為編譯器會将所有的泛型資訊擦除,那麼運作時也就無能擷取到具體的泛型類型了。但其實并不是這樣,如上面介紹到,JDK1.5之後,Class的格式有變化,編譯器會将聲明的類,接口,方法的泛型資訊保留到位元組碼當中。那麼通過反射,這些資訊還是可以擷取到的。但要擷取到具體的泛型類型,一般也隻能擷取到繼承父類所使用的泛型類型。 

比如:

public class SubClass extends Base<String> { }11      

那麼Base所綁定的泛型類型可以被擷取到的。對SubClass.class調用

getGenericSuperclass

可以擷取到T所綁定的類型。

        Type type = SubClass.class.getGenericSuperclass();
        Type targ = ((ParameterizedType) type).getActualTypeArguments()[0];
        System.out.println(type); // SubClass<java.lang.String>
        System.out.println(targ); // class java.lang.String12341234      

具體的用法可以參考Gson和Guice的源碼:

  • https://github.com/google/guice/blob/abc78c361d9018da211690b673accb580a52abf2/core/src/com/google/inject/TypeLiteral.java#L94
  • https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/internal/%24Gson%24Types.java

橋方法

為了使Java的泛型方法生成的位元組碼與1.5以前的位元組碼相相容,由編譯期自己生成的方法。顧名思義,橋方法是一座橋,溝通着泛型與多态。

可以通過

Method.isBridge()

方法來判斷一個方法是否是橋接方法,在位元組碼中橋接方法會被标記為

ACC_BRIDGE

ACC_SYNTHETIC

public class Fruit<T> {
    T value;
    public T getValue() {
        return value;
    }
}
public class Apple extends Fruit<String> {
    @Override
    public String getValue() {
        return "foo was call";
    }
}123456789101112123456789101112      

反編譯生成的位元組碼:

public class Apple extends Fruit<java.lang.String> {
  public Apple();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Fruit."<init>":()V
       4: return

  public java.lang.String getValue();
    Code:
       0: ldc           #2                  // String calling
       2: areturn

  public java.lang.Object getValue();
    Code:
       0: aload_0
       1: invokevirtual #3                  // Method getValue:()Ljava/lang/String;
       4: areturn
}123456789101112131415161718123456789101112131415161718      

編譯器為我們自動生成了有一個橋方法,這個橋方法傳回類型為Object,内部調用了我們自定義的另一個getValue方法。