【轉】Java語法糖的味道:泛型與類型擦除

        泛型是JDK 1.5的一項新特性,它的本質是參數化類型(Parameterized Type)的應用,也就是說所操做的數據類型被指定爲一個參數。這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口和泛型方法。 java

        泛型思想早在C++語言的模板(Templates)中就開始生根發芽,在Java語言處於尚未出現泛型的版本時,只能經過Object是全部類型的父類和類型強制轉換兩個特色的配合來實現類型泛化。例如在哈希表的存取中,JDK 1.5以前使用HashMap的get()方法,返回值就是一個Object對象,因爲Java語言裏面全部的類型都繼承於java.lang.Object,那Object轉型成任何對象都是有可能的。可是也由於有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object究竟是個什麼類型的對象。在編譯期間,編譯器沒法檢查這個Object的強制轉型是否成功,若是僅僅依賴程序員去保障這項操做的正確性,許多ClassCastException的風險就會被轉嫁到程序運行期之中。 程序員

        泛型技術在C#和Java之中的使用方式看似相同,但實現上卻有着根本性的分歧,C#裏面泛型不管在程序源碼中、編譯後的IL中(Intermediate Language,中間語言,這時候泛型是一個佔位符)或是運行期的CLR中都是切實存在的,List<int>與List<String>就是兩個不一樣的類型,它們在系統運行期生成,有本身的虛方法表和類型數據,這種實現稱爲類型膨脹,基於這種方法實現的泛型被稱爲真實泛型。 數據結構

        Java語言中的泛型則不同,它只在程序源碼中存在,在編譯後的字節碼文件中,就已經被替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,而且在相應的地方插入了強制轉型代碼,所以對於運行期的Java語言來講,ArrayList<int>與ArrayList<String>就是同一個類。因此說泛型技術其實是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型被稱爲僞泛型。 工具

        代碼清單10-2是一段簡單的Java泛型例子,咱們能夠看一下它編譯後的結果是怎樣的? 代碼清單 10-2 泛型擦除前的例子性能

public static void main(String[] args) { 
    Map<String, String> map = new HashMap<String, String>(); 
    map.put("hello", "你好"); 
    map.put("how are you?", "吃了沒?"); 
    System.out.println(map.get("hello")); 
    System.out.println(map.get("how are you?")); 
}

把這段Java代碼編譯成Class文件,而後再用字節碼反編譯工具進行反編譯後,將會發現泛型都不見了,程序又變回了Java泛型出現以前的寫法,泛型類型都變回了原生類型,如代碼清單10-3所示。 
代碼清單 10-3 泛型擦除後的例子測試

public static void main(String[] args) { 
    Map map = new HashMap(); 
    map.put("hello", "你好"); 
    map.put("how are you?", "吃了沒?"); 
    System.out.println((String) map.get("hello")); 
    System.out.println((String) map.get("how are you?")); 
}

        當初JDK設計團隊爲何選擇類型擦除的方式來實現Java語言的泛型支持呢?是由於實現簡單、兼容性考慮仍是別的緣由?咱們已不得而知,但確實有很多人對Java語言提供的僞泛型很有微詞,當時甚至連《Thinking In Java》一書的做者Bruce Eckel也發表了一篇文章《這不是泛型!》 來批評JDK 1.5中的泛型實現。 
(注1:原文:http://www.anyang-window.com.cn/quotthis-is-not-a-genericquot-bruce-eckel-eyes-of-the-generic-java/) 
當時衆多的批評之中,有一些是比較表面的,還有一些從性能上說泛型會因爲強制轉型操做和運行期缺乏針對類型的優化等從而致使比C#的泛型慢一些,則是徹底偏離了方向,姑且不論Java泛型是否是真的會比C#泛型慢,選擇從性能的角度上評價用於提高語義準確性的泛型思想,就猶如在討論劉翔打斯諾克的水平與丁俊暉有多大的差距通常。但筆者也並不是在爲Java的泛型辯護,它在某些場景下確實存在不足,筆者認爲經過擦除法來實現泛型喪失了一些泛型思想應有的優雅,例以下面代碼清單10-4的例子: 優化

public class GenericTypes { 
    public static void method(List<String> list) { 
        System.out.println("invoke method(List<String> list)"); 
    } 
 
    public static void method(List<Integer> list) { 
       System.out.println("invoke method(List<Integer> list)"); 
    } 
}

請想想,上面這段代碼是否正確,可否編譯執行?也許您已經有了答案,這段代碼是不能被編譯的,是由於參數List<Integer>和List<String>編譯以後都被擦除了,變成了同樣的原生類型List<E>,擦除動做致使這兩個方法的特徵簽名變得如出一轍。初步看來,沒法重載的緣由已經找到了,可是真的就是如此嗎?只能說,泛型擦除成相同的原生類型只是沒法重載的其中一部分緣由,請再接着看一看代碼清單10-5中的內容:  this

public class GenericTypes { 
    public static String method(List<String> list) { 
        System.out.println("invoke method(List<String> list)"); 
        return ""; 
    } 
 	 
    public static int method(List<Integer> list) { 
        System.out.println("invoke method(List<Integer> list)"); 
	return 1; 
    } 
	 
    public static void main(String[] args) { 
        method(new ArrayList<String>()); 
        method(new ArrayList<Integer>()); 
    } 
}

執行結果:編碼

invoke method(List<String> list) 
invoke method(List<Integer> list)

        代碼清單10-5與代碼清單10-4的差異,是兩個method方法添加了不一樣的返回值,因爲這兩個返回值的加入,方法重載竟然成功了,即這段代碼能夠被編譯和執行 了。這是咱們對Java語言中返回值不參與重載選擇的基本認知的挑戰嗎? 

(注2:測試的時候請使用Sun JDK的Javac編譯器進行編譯,其餘編譯器,如Eclipse JDT的ECJ編譯器,仍然可能會拒絕編譯這段代碼,ECJ編譯時會提示「Method method(List<String>) has the same erasure method(List<E>) as another method in type GenericTypes」。) spa

        代碼清單10-5中的重載固然不是根據返回值來肯定的,之因此此次能編譯和執行成功,是由於兩個mehtod()方法加入了不一樣的返回值後才能共存在一個Class文件之中。第6章介紹Class文件方法表(method_info)的數據結構時曾經提到過,方法重載要求方法具有不一樣的特徵簽名,返回值並不包含在方法的特徵簽名之中,因此返回值不參與重載選擇,可是在Class文件格式之中,只要描述符不是徹底一致的兩個方法就能夠共存。也就是說兩個方法若是有相同的名稱和特徵簽名,但返回值不一樣,那它們也是能夠合法地共存於一個Class文件中的。 

        因爲Java泛型的引入,各類場景(虛擬機解析、反射等)下的方法調用均可能對原有的基礎產生影響和新的需求,如在泛型類中如何獲取傳入的參數化類型等。因此JCP組織對虛擬機規範作出了相應的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用於解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的做用就是存儲一個方法在字節碼層面的特徵簽名 ,這個屬性中保存的參數類型並非原生類型,而是包括了參數化類型的信息。修改後的虛擬機規範 要求全部能識別49.0以上版本的Class文件的虛擬機都要能正確地識別Signature參數。 (注3:在《Java虛擬機規範第二版》(JDK 1.5修改後的版本)的「§4.4.4 Signatures」章節及《Java語言規範第三版》的「§8.4.2 Method Signature」章節中分別都定義了字節碼層面的方法特徵簽名,以及Java代碼層面的方法特徵簽名,特徵簽名最重要的任務就是做爲方法獨一無二不可重複的ID,在Java代碼中的方法特徵簽名只包括了方法名稱、參數順序及參數類型,而在字節碼中的特徵簽名還包括方法返回值及受查異常表,本書中若是指的是字節碼層面的方法簽名,筆者會加入限定語進行說明,也請讀者根據上下文語境注意區分。) 

        從上面的例子能夠看到擦除法對實際編碼帶來的影響,因爲List<String>和List<Integer>擦除後是同一個類型,咱們只能添加兩個並不須要實際使用到的返回值才能完成重載,這是一種毫無優雅和美感可言的解決方案。同時,從Signature屬性的出現咱們還能夠得出結論,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中仍是保留了泛型信息,這也是咱們能經過反射手段取得參數化類型的根本依據。

相關文章
相關標籤/搜索