一、什麼是泛型擦除
泛型(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屬性。