深入理解系列之JAVA泛型機制
泛型是指在宣告(類,方法,屬性)的時候採用一個“標誌符”來代替,而只有在呼叫的時候才傳入真正的型別,我們最常見的泛型例項就是前面講述的集合類,集合類在宣告的時候都是通過泛型方式來宣告的,只有在呼叫(例項化)時我們才確定傳入的是Integer亦或是String等等!
注:本文著重敘述泛型實現的原理,而忽略一些泛型應用時的注意事項,詳細應用時的注意事項請參看其他博文
問題一、為什麼要採用泛型?
泛型機制是JDK1.5出現的。拿ArrayList舉例,在JDK1.5出現之前,為了解決儲存不同引數型別資料的問題,ArrayList宣告的時候傳入引數定義為Object,因為Object是所有型別的父類,這樣在取出的時候再通過手動的強制轉換為實際的型別。大概的實現是這樣的(原理性描述):
class ArrayList_Before_JDK5{
private Object[] elements=new Object[10];
public Object get(int i){
return elements[i];
}
public void add(Object o){
if(elements.length > 1)
elements[1] = o;
elements[0] = o;
}
}
這樣我們在使用ArrayList的時候,將會這樣用:
public static void main(String[] args) {
ArrayList_Before_JDK5 arrayList_before_jdk5 = new ArrayList_Before_JDK5();
arrayList_before_jdk5.add("123");
arrayList_before_jdk5.add(123);
String string = (String)arrayList_before_jdk5.get(0);
Integer integer = (Integer) arrayList_before_jdk5.get(1);
}
我們這裡發現兩個問題:
1、當我們獲取某一個值的時候必須手動強轉;
2、假設我們想把這個例項全部存入String型別的,由於底層的引數是Object,所以程式並不會阻止我們傳入Integer型別的引數,這時如果我們仍然使用:
String string = (String) arrayList_before_jdk5.get(1);
獲取位置1的資料(注意此時我們認為存入的是string,所以我們都使用String強轉符,但是實際上位置1被我們傳入了Interger型別)時,程式在編譯期間不會出現任何錯誤,但是執行的時候卻會出現異常:
Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at Fxing.main(Fxing.java:10)
能不能通過一種機制在編譯的時候就(或者說在IDE語法提示的時候)就能提前檢測出錯誤,避免不必要的執行異常呢?答案是肯定的,就是泛型!採用泛型設計的ArrayList將會是這樣的(JDK8原始碼,但是剔除了不必要的原始碼):
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
transient Object[] elementData;
public boolean add(E e) {
elementData[size++] = e;
return true;
}
public E get(int index) {
return elementData(index);
}
這裡的E就是在ArrayList中的“泛型標誌符”,我們在建立例項的時候就會依照自己的需求傳入String、Integer等等,那麼在底層這個E就變成了String、Integer等等,所以傳入引數的時候就必須按照相應的型別來寫入了!
問題二、泛型的底層原理是什麼?
我們上面說過,當依照自己的需求傳入實際的型別引數的時候,E將會變成實際的型別引數——泛型追求的效果就是這樣的,但是在JVM編譯後其實並不是這樣的,我們稱之為“泛型擦除”!也就是說,編譯過後的位元組碼將會還原回原始型別——和JDK5之前的一樣。所以,我們也稱JAVA的泛型為偽泛型!為了證明這一點,我們通過兩種反編譯方法來驗證,以下面程式碼為例:
public class Fxing {
public static void main(String[] args) {
ArrayList<String> arrayList_string = new ArrayList<>();
ArrayList<Integer> arrayList_integer = new ArrayList<>();
arrayList_integer.add(1);
arrayList_string.add("1");
System.out.println(arrayList_integer.get(0));
System.out.println(arrayList_string.get(0));
}
}
第一:IDE反編譯位元組碼
如果你使用IDE反編譯位元組碼,你會發現下面的情形:
public class Fxing {
public Fxing() {
}
public static void main(String[] args) {
ArrayList<String> arrayList_string = new ArrayList();
ArrayList<Integer> arrayList_integer = new ArrayList();
arrayList_integer.add(Integer.valueOf(1));
arrayList_string.add("1");
System.out.println(arrayList_integer.get(0));
System.out.println((String)arrayList_string.get(0));
}
}
不是說編譯位元組碼的時候會發生“泛型擦除”迴歸到原始型別嗎?為什麼我們可以看到String、Integer型別的呢?這個問題在部落格 關於java泛型擦除反編譯後泛型會出現問題也同樣出現了,同時在《深入理解JVM虛擬機器》中也找到了佐證:
JCP組織對虛擬機器規範做出了相應的修改,引入了諸如Signature、Loca-lVariableTypeTable等新的屬性用於解決伴隨泛型而來的引數型別的識別問題,Signature是其中最重要的一項屬性,它的作用就是儲存一個方法在位元組碼層面的特徵簽名,這個屬性中儲存的引數型別並不是原生型別 ,而是包括了引數化型別的資訊。修改後的虛擬機器規範要求所有能識別49.0以上版本的Class檔案的虛擬機器都要能正確地識別Signature引數。
通俗點將就是,JCP組織要求class檔案儲存實際型別資訊,所以IDE可以由特殊欄位獲取實際型別,從而智慧的反編譯!所以事實上,反編譯後的程式碼是這樣的:
public class Fxing {
public Fxing() {
}
public static void main(String[] args) {
ArrayList arrayList_string = new ArrayList();
ArrayList arrayList_integer = new ArrayList();
arrayList_integer.add(Integer.valueOf(1));
arrayList_string.add("1");
System.out.println(arrayList_integer.get(0));
System.out.println((String)arrayList_string.get(0));
}
}
方法二:javap反編譯
如果上述還是不能使你信服,我們可以通過javap(javap -c XXX.class)編譯出位元組碼指令來看看到底發生了什麼:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: new #2 // class java/util/ArrayList
11: dup
12: invokespecial #3 // Method java/util/ArrayList."<init>":()V
15: astore_2
16: aload_2
17: iconst_1
18: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
24: pop
25: aload_1
26: ldc #6 // String 1
28: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
31: pop
32: aload_2
33: iconst_0
34: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
37: pop
38: aload_1
39: iconst_0
40: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
43: pop
44: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
47: aload_2
48: iconst_0
49: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
52: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
55: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
58: aload_1
59: iconst_0
60: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
63: checkcast #10 // class java/lang/String
66: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
69: return
}
我們可以看到(4、12)(21、28、34、40),初始化的時候並沒有攜帶實際型別資訊,add、get的時候使用的都是object!同時當需要獲取最終資料的時候,JVM虛擬機器將自動強制轉型(63、66),所以我們需要注意的是:
泛型是偽泛型,即使泛型物件是傳入不同的型別引數泛型物件,因為在編譯階段會被擦除,所以實際上該泛型物件屬於同一個類,進而在函式過載的時候不會因為泛型引數不同而“過載成功”!
類似還用其他類似引用傳遞問題、萬用字元問題請移步部落格:Java泛型深入理解檢視!