Java泛型的类型擦除
寫在前面:最近在看泛型,研究泛型的過程中,發現了一個比較令我意外的情況,Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。 其實編譯器通過Code sharing方式為每個泛型類型創建唯一的字節碼表示,并且將該泛型類型的實例都映射到這個唯一的字節碼表示上。將多種泛型類形實例映射到唯一的字節碼表示是通過類型擦除(type erasue)實現的。
?
類型擦除,嘿嘿,第一次聽說的東西,很好奇,于是上網查了查,把官方解釋貼在下面,應該可以看得懂JavaDoc
Type Erasure?Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to: Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods. Insert type casts if necessary to preserve type safety. Generate bridge methods to preserve polymorphism in extended generic types. Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.
一、各種語言中的編譯器是如何處理泛型的
通常情況下,一個編譯器處理泛型有兩種方式:
1.Code specialization。在實例化一個泛型類或泛型方法時都產生一份新的目標代碼(字節碼or二進制代碼)。例如,針對一個泛型list,可能需要 針對string,integer,float產生三份目標代碼。
2.Code sharing。對每個泛型類只生成唯一的一份目標代碼;該泛型類的所有實例都映射到這份目標代碼上,在需要的時候執行類型檢查和類型轉換。
C++中的模板(template)是典型的Code specialization實現。C++編譯器會為每一個泛型類實例生成一份執行代碼。執行代碼中integer list和string list是兩種不同的類型。這樣會導致代碼膨脹(code bloat)。?C#里面泛型無論在程序源碼中、編譯后的IL中(Intermediate Language,中間語言,這時候泛型是一個占位符)或是運行期的CLR中都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的虛方法表和類型數據,這種實現稱為類型膨脹,基于這種方法實現的泛型被稱為真實泛型。?Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯后的字節碼文件中,就已經被替換為原來的原生類型(Raw Type,也稱為裸類型)了,并且在相應的地方插入了強制轉型代碼,因此對于運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類。所以說泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為類型擦除,基于這種方法實現的泛型被稱為偽泛型。
C++和C#是使用Code specialization的處理機制,前面提到,他有一個缺點,那就是會導致代碼膨脹。另外一個弊端是在引用類型系統中,浪費空間,因為引用類型集合中元素本質上都是一個指針。沒必要為每個類型都產生一份執行代碼。而這也是Java編譯器中采用Code sharing方式處理泛型的主要原因。
Java編譯器通過Code sharing方式為每個泛型類型創建唯一的字節碼表示,并且將該泛型類型的實例都映射到這個唯一的字節碼表示上。將多種泛型類形實例映射到唯一的字節碼表示是通過類型擦除(type erasue)實現的。
二、什么是類型擦除
前面我們多次提到這個詞:類型擦除(type erasue)**,那么到底什么是類型擦除呢?
類型擦除指的是通過類型參數合并,將泛型類型實例關聯到同一份字節碼上。編譯器只為泛型類型生成一份字節碼,并將其實例關聯到這份字節碼上。類型擦除的關鍵在于從泛型類型中清除類型參數的相關信息,并且再必要的時候添加類型檢查和類型轉換的方法。 類型擦除可以簡單的理解為將泛型java代碼轉換為普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通java字節碼。 類型擦除的主要過程如下: 1.將所有的泛型參數用其最左邊界(最頂級的父類型)類型替換。(這部分內容可以看:Java泛型中extends和super的理解) 2.移除所有的類型參數。
三、Java編譯器處理泛型的過程
code 1:
public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("name", "hollis"); map.put("age", "22"); System.out.println(map.get("name")); System.out.println(map.get("age")); }反編譯后的code 1:
public static void main(String[] args) { Map map = new HashMap(); map.put("name", "hollis"); map.put("age", "22"); System.out.println((String) map.get("name")); System.out.println((String) map.get("age")); }我們發現泛型都不見了,程序又變回了Java泛型出現之前的寫法,泛型類型都變回了原生類型,
code 2:
interface Comparable<A> {public int compareTo(A that); }public final class NumericValue implements Comparable<NumericValue> {private byte value;public NumericValue(byte value) {this.value = value;}public byte getValue() {return value;}public int compareTo(NumericValue that) {return this.value - that.value;} }反編譯后的code 2:
interface Comparable {public int compareTo( Object that); } public final class NumericValueimplements Comparable {public NumericValue(byte value){this.value = value;}public byte getValue(){return value;}public int compareTo(NumericValue that){return value - that.value;}public volatile int compareTo(Object obj){return compareTo((NumericValue)obj);}private byte value; }code 3:
public class Collections {public static <A extends Comparable<A>> A max(Collection<A> xs) {Iterator<A> xi = xs.iterator();A w = xi.next();while (xi.hasNext()) {A x = xi.next();if (w.compareTo(x) < 0)w = x;}return w;} }反編譯后的code 3:
public class Collections {public Collections(){}public static Comparable max(Collection xs){Iterator xi = xs.iterator();Comparable w = (Comparable)xi.next();while(xi.hasNext()){Comparable x = (Comparable)xi.next();if(w.compareTo(x) < 0)w = x;}return w;} }第2個泛型類Comparable <A>擦除后 A被替換為最左邊界Object。Comparable<NumericValue>的類型參數NumericValue被擦除掉,但是這直 接導致NumericValue沒有實現接口Comparable的compareTo(Object that)方法,于是編譯器充當好人,添加了一個橋接方法。 第3個示例中限定了類型參數的邊界<A extends Comparable<A>>A,A必須為Comparable<A>的子類,按照類型擦除的過程,先講所有的類型參數 ti換為最左邊界Comparable<A>,然后去掉參數類型A,得到最終的擦除后結果。
四、泛型帶來的問題
一、當泛型遇到重載:
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<String>另一個是List<Integer>?,但是,這段代碼是編譯通不過的。因為我們前面講過,參數List<Integer>和List<String>編譯之后都被擦除了,變成了一樣的原生類型List,擦除動作導致這兩個方法的特征簽名變得一模一樣。
二、當泛型遇到catch:
如果我們自定義了一個泛型異常類GenericException,那么,不要嘗試用多個catch取匹配不同的異常類型,例如你想要分別捕獲GenericException、GenericException,這也是有問題的。
三、當泛型內包含靜態變量
public class StaticTest{public static void main(String[] args){GT<Integer> gti = new GT<Integer>();gti.var=1;GT<String> gts = new GT<String>();gts.var=2;System.out.println(gti.var);} } class GT<T>{public static int var=0;public void nothing(T x){} }答案是——2!由于經過類型擦除,所有的泛型類實例都關聯到同一份字節碼上,泛型類的所有靜態變量是共享的。
五、總結
1.虛擬機中沒有泛型,只有普通類和普通方法,所有泛型類的類型參數在編譯時都會被擦除,泛型類并沒有自己獨有的Class類對象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。 2.創建泛型對象時請指明類型,讓編譯器盡早的做參數檢查(Effective Java,第23條:請不要在新代碼中使用原生態類型) 3.不要忽略編譯器的警告信息,那意味著潛在的ClassCastException等著你。 4.靜態變量是被泛型類的所有實例所共享的。對于聲明為MyClass<T>的類,訪問其中的靜態變量的方法仍然是?MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創建的對象,都是共享一個靜態變量。 5.泛型的類型參數不能用在Java異常處理的catch語句中。因為異常處理是由JVM在運行時刻來進行的。由于類型信息被擦除,JVM是無法區分兩個異常類型MyException<String>和MyException<Integer>的。對于JVM來說,它們都是?MyException類型的。也就無法執行與異常對應的catch語句。
from:?https://www.hollischuang.com/archives/226?
總結
以上是生活随笔為你收集整理的Java泛型的类型擦除的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java语言 泛型 类型擦除
- 下一篇: [Google Guava] 7-原生类