Java泛型的类型擦除
寫在前面:最近在看泛型,研究泛型的過程中,發(fā)現(xiàn)了一個比較令我意外的情況,Java中的泛型基本上都是在編譯器這個層次來實(shí)現(xiàn)的。在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數(shù),會被編譯器在編譯的時候去掉。 其實(shí)編譯器通過Code sharing方式為每個泛型類型創(chuàng)建唯一的字節(jié)碼表示,并且將該泛型類型的實(shí)例都映射到這個唯一的字節(jié)碼表示上。將多種泛型類形實(shí)例映射到唯一的字節(jié)碼表示是通過類型擦除(type erasue)實(shí)現(xiàn)的。
?
類型擦除,嘿嘿,第一次聽說的東西,很好奇,于是上網(wǎng)查了查,把官方解釋貼在下面,應(yīng)該可以看得懂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。在實(shí)例化一個泛型類或泛型方法時都產(chǎn)生一份新的目標(biāo)代碼(字節(jié)碼or二進(jìn)制代碼)。例如,針對一個泛型list,可能需要 針對string,integer,float產(chǎn)生三份目標(biāo)代碼。
2.Code sharing。對每個泛型類只生成唯一的一份目標(biāo)代碼;該泛型類的所有實(shí)例都映射到這份目標(biāo)代碼上,在需要的時候執(zhí)行類型檢查和類型轉(zhuǎn)換。
C++中的模板(template)是典型的Code specialization實(shí)現(xiàn)。C++編譯器會為每一個泛型類實(shí)例生成一份執(zhí)行代碼。執(zhí)行代碼中integer list和string list是兩種不同的類型。這樣會導(dǎo)致代碼膨脹(code bloat)。?C#里面泛型無論在程序源碼中、編譯后的IL中(Intermediate Language,中間語言,這時候泛型是一個占位符)或是運(yùn)行期的CLR中都是切實(shí)存在的,List<int>與List<String>就是兩個不同的類型,它們在系統(tǒng)運(yùn)行期生成,有自己的虛方法表和類型數(shù)據(jù),這種實(shí)現(xiàn)稱為類型膨脹,基于這種方法實(shí)現(xiàn)的泛型被稱為真實(shí)泛型。?Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)被替換為原來的原生類型(Raw Type,也稱為裸類型)了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼,因此對于運(yùn)行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類。所以說泛型技術(shù)實(shí)際上是Java語言的一顆語法糖,Java語言中的泛型實(shí)現(xiàn)方法稱為類型擦除,基于這種方法實(shí)現(xiàn)的泛型被稱為偽泛型。
C++和C#是使用Code specialization的處理機(jī)制,前面提到,他有一個缺點(diǎn),那就是會導(dǎo)致代碼膨脹。另外一個弊端是在引用類型系統(tǒng)中,浪費(fèi)空間,因為引用類型集合中元素本質(zhì)上都是一個指針。沒必要為每個類型都產(chǎn)生一份執(zhí)行代碼。而這也是Java編譯器中采用Code sharing方式處理泛型的主要原因。
Java編譯器通過Code sharing方式為每個泛型類型創(chuàng)建唯一的字節(jié)碼表示,并且將該泛型類型的實(shí)例都映射到這個唯一的字節(jié)碼表示上。將多種泛型類形實(shí)例映射到唯一的字節(jié)碼表示是通過類型擦除(type erasue)實(shí)現(xiàn)的。
二、什么是類型擦除
前面我們多次提到這個詞:類型擦除(type erasue)**,那么到底什么是類型擦除呢?
類型擦除指的是通過類型參數(shù)合并,將泛型類型實(shí)例關(guān)聯(lián)到同一份字節(jié)碼上。編譯器只為泛型類型生成一份字節(jié)碼,并將其實(shí)例關(guān)聯(lián)到這份字節(jié)碼上。類型擦除的關(guān)鍵在于從泛型類型中清除類型參數(shù)的相關(guān)信息,并且再必要的時候添加類型檢查和類型轉(zhuǎn)換的方法。 類型擦除可以簡單的理解為將泛型java代碼轉(zhuǎn)換為普通java代碼,只不過編譯器更直接點(diǎn),將泛型java代碼直接轉(zhuǎn)換成普通java字節(jié)碼。 類型擦除的主要過程如下: 1.將所有的泛型參數(shù)用其最左邊界(最頂級的父類型)類型替換。(這部分內(nèi)容可以看:Java泛型中extends和super的理解) 2.移除所有的類型參數(shù)。
三、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")); }我們發(fā)現(xiàn)泛型都不見了,程序又變回了Java泛型出現(xiàn)之前的寫法,泛型類型都變回了原生類型,
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>的類型參數(shù)NumericValue被擦除掉,但是這直 接導(dǎo)致NumericValue沒有實(shí)現(xiàn)接口Comparable的compareTo(Object that)方法,于是編譯器充當(dāng)好人,添加了一個橋接方法。 第3個示例中限定了類型參數(shù)的邊界<A extends Comparable<A>>A,A必須為Comparable<A>的子類,按照類型擦除的過程,先講所有的類型參數(shù) ti換為最左邊界Comparable<A>,然后去掉參數(shù)類型A,得到最終的擦除后結(jié)果。
四、泛型帶來的問題
一、當(dāng)泛型遇到重載:
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)"); } }上面這段代碼,有兩個重載的函數(shù),因為他們的參數(shù)類型不同,一個是List<String>另一個是List<Integer>?,但是,這段代碼是編譯通不過的。因為我們前面講過,參數(shù)List<Integer>和List<String>編譯之后都被擦除了,變成了一樣的原生類型List,擦除動作導(dǎo)致這兩個方法的特征簽名變得一模一樣。
二、當(dāng)泛型遇到catch:
如果我們自定義了一個泛型異常類GenericException,那么,不要嘗試用多個catch取匹配不同的異常類型,例如你想要分別捕獲GenericException、GenericException,這也是有問題的。
三、當(dāng)泛型內(nèi)包含靜態(tài)變量
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!由于經(jīng)過類型擦除,所有的泛型類實(shí)例都關(guān)聯(lián)到同一份字節(jié)碼上,泛型類的所有靜態(tài)變量是共享的。
五、總結(jié)
1.虛擬機(jī)中沒有泛型,只有普通類和普通方法,所有泛型類的類型參數(shù)在編譯時都會被擦除,泛型類并沒有自己獨(dú)有的Class類對象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。 2.創(chuàng)建泛型對象時請指明類型,讓編譯器盡早的做參數(shù)檢查(Effective Java,第23條:請不要在新代碼中使用原生態(tài)類型) 3.不要忽略編譯器的警告信息,那意味著潛在的ClassCastException等著你。 4.靜態(tài)變量是被泛型類的所有實(shí)例所共享的。對于聲明為MyClass<T>的類,訪問其中的靜態(tài)變量的方法仍然是?MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創(chuàng)建的對象,都是共享一個靜態(tài)變量。 5.泛型的類型參數(shù)不能用在Java異常處理的catch語句中。因為異常處理是由JVM在運(yùn)行時刻來進(jìn)行的。由于類型信息被擦除,JVM是無法區(qū)分兩個異常類型MyException<String>和MyException<Integer>的。對于JVM來說,它們都是?MyException類型的。也就無法執(zhí)行與異常對應(yīng)的catch語句。
from:?https://www.hollischuang.com/archives/226?
總結(jié)
以上是生活随笔為你收集整理的Java泛型的类型擦除的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java语言 泛型 类型擦除
- 下一篇: 高级开发必须理解的Java中SPI机制