天天看點

【JAVA】對泛型擦除的一點思考

一、什麼是泛型擦除

泛型(generics)的真正面目,是參數化對象類型。在使用泛型的時候,我們總是把一個具體的對象類型當作一個參數傳入。

泛型的作用就是發生在編譯時,它提供了安全檢查機制。

可是當處于編譯時,所有的泛型都會被去掉,即被還原為原始類型,如java.util.ArrayList,不再有"<T>"。

二、代碼驗證

建立一個List<String>與List<Integer>

List<String> stringList = new ArrayList<>();
        stringList.add("123");
        //這句報錯,idea提示隻能插入String類型
        //如果我們在記事本中這樣寫,使用javac編譯時,就會報錯
        //stringList.add(123);
        
        List<Integer> integerList = new ArrayList<>();

        System.out.println(stringList.getClass());
        System.out.println(integerList.getClass());      

運作後,輸出同樣的類型。

class java.util.ArrayList
        class java.util.ArrayList      

這和例子說明:在編譯時,編譯器會進行安全檢查。編譯後,泛型的類型全部被擦除,隻剩下了原始類型。

三、在位元組碼指令中觀察類型擦除

原始代碼:

public class Main<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        Main<String> s = new Main<>();
        s.setT("abc");
        String str = s.getT();
        System.out.println(str);
    }

}      

使用javap -c Main.class反編譯後得到:

public class com.yang.testGenerics.Main<T> {
  public com.yang.testGenerics.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public T getT();
    Code:
       0: aload_0
       1: getfield      #2                  // Field t:Ljava/lang/Object;
       4: areturn

  public void setT(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field t:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class com/yang/testGenerics/Main
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String abc
      11: invokevirtual #6                  // Method setT:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getT:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: return
}      

反編譯後,在main方法中,可以發現,set進去的是一個原始類型Object。

第15行,get擷取的也是一個Object類型。

重點在于第18行,做了一個checkcast類型轉換,将Object強轉為了String。

可以看得出,泛型在生成的位元組碼中,就已經被去掉了,是以在運作時,List<String>與List<Integer>都是一個類。

那麼,如果我們在一個類中聲明以下的方法:

private int add(List<Integer> integerList) {
        return 1;
    }

    private double add(List<String> stringList) {
        return 1.0;
    }      

這樣的代碼,無法通過編譯。首先方法的傳回值是不參與重載選擇的,也就是重載不看傳回值。此外,泛型的擦除使得方法的特征簽名完全一樣,是以這裡可以看做是重複的方法,是以編譯失敗。

四、真的無法在運作時擷取泛型類型嗎?

看以下的代碼:

public class Test {

    private List<Integer> list;

    public static void main(String[] args) {
        try {
            Field field = Test.class.getDeclaredField("list");
            System.out.println(field.getGenericType());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

}      

運作後,會輸出:

java.util.List<java.lang.Integer>      

泛型的類型,确實拿到了,這是怎麼回事?

由于Java泛型的實作機制,使用了泛型的代碼在運作期間相關的泛型參數的類型會被擦除,我們無法在運作期間獲知泛型參數的具體類型(所有的泛型類型在運作時都是Object類型)。但是在編譯java源代碼成 class檔案中還是儲存了泛型相關的資訊,這些資訊被儲存在class位元組碼常量池中,使用了泛型的代碼處會生成一個signature簽名字段,通過簽名signature字段指明這個常量池的位址,通過反射擷取泛型參數類型,歸根結底都是來源于這個signature屬性。

五、總結