泛型實現原理


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);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM