Java中的泛型是偽泛型
泛型思想最早在C++語言的模板(Templates)中產生,Java后來也借用了這種思想。雖然思想一致,但是他們存在着本質性的不同。C++中的模板是真正意義上的泛型,在編譯時就將不同模板類型參數編譯成對應不同的目標代碼,ClassName和ClassName是兩種不同的類型,這種泛型被稱為真正泛型。這種泛型實現方式,會導致類型膨脹,因為要為不同具體參數生成不同的類。
Java中ClassName和ClassName雖然在源代碼中屬於不同的類,但是編譯后的字節碼中,他們都被替換成原始類型(ClassName),而兩者的原始類型的一樣的,所以在運行時環境中,ClassName和ClassName就是同一個類。Java中的泛型是一種特殊的語法糖,通過類型擦除實現(后面介紹),這種泛型稱為偽泛型。由於Java中有這么一個障眼法,如果沒有進行深入研究,就會在產生莫名其妙的問題。值得一提的是,不少大牛對Java的泛型的實現方式很不滿意。
類型擦除
Java中的泛型是通過類型擦除來實現的。所謂類型擦除,是指通過類型參數合並,將泛型類型實例關聯到同一份字節碼上。編譯器只為泛型類型生成一份字節碼,並將其實例關聯到這份字節碼上。類型擦除的關鍵在於從泛型類型中清除類型參數的相關信息,並且再必要的時候添加類型檢查和類型轉換的方法。
下面通過兩個例子來證明在編譯時確實發生了類型擦除。
例1分別創建實際類型為String和Integer的ArrayList對象,通過getClass()方法獲取兩個實例的類,最后判斷這個實例的類是相等的,證明兩個實例共享同一個類。
// 聲明一個具體類型為String的ArrayList
ArrayList<String> arrayList1 = new ArrayList<String>();
arrayList1.add("abc");
// 聲明一個具體類型為Integer的ArrayList
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass() == arrayList2.getClass()); // 結果為true
例2創建一個只能存儲Integer的ArrayList對象,在add一個整型數值后,利用反射調用add(Object o)add一個asd字符串,此時運行代碼不會報錯,運行結果會打印出1和asd兩個值。這時再里利用反射調用add(Integer o)方法,運行會拋出codeNoSuchMethodException異常。這充分證明了在編譯后,擦除了Integer這個泛型信息,只保留了原始類型。
ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
arrayList3.add(1);
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i = 0; i < arrayList3.size(); i++) {
System.out.println(arrayList3.get(i)); // 輸出1,asd
}
arrayList3.getClass().getMethod("add", Integer.class).invoke(arrayList3, 2); // NoSuchMethodException:java.util.ArrayList.add(java.lang.Integer)
自動類型轉換
上一節上說到了類型擦除,Java編譯器會擦除掉泛型信息。那么調用ArrayList的get()最終返回的必然會是一個Object對象,但是我們在源代碼並沒有寫過Object轉成Integer的代碼,為什么就能“直接”將取出來的對象賦予一個Integer類型的變量呢(如下面的代碼第12行)?
import java.util.List;
import java.util.ArrayList;
/**
* 泛型中的類型轉換測試。
*/
public class Test {
public static void main(String[] args) {
List<Integer> a = new ArrayList<Integer>();
a.add(1);
Integer ai = a.get(0);
}
}
實際上,Java的泛型除了類型擦除之外,還會自動生成checkcast指令進行強制類型轉換。上面的代碼中的main方法編譯后所對應的字節碼如下。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: aload_1
20: iconst_0
21: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
26: checkcast #7 // class java/lang/Integer
29: astore_2
30: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 19
line 10: 30
}
看到第18行代碼就是將Object類型的對象強制轉換為Integer的指令。我們完全可以將上面的代碼轉換為下面的代碼,它所實現的效果跟上面的泛型是一模一樣的。既然泛型也需要進行強制轉換,所以泛型並不會提供運行時效率,不過可以大大降低編程時的出錯概率。
public static void main(String[] args) {
List a = new ArrayList();
a.add(1);
Integer ai = (Integer)a.get(0);
}