Java的型別擦除
一、各種語言中的編譯器是如何處理泛型的
通常情況下,一個編譯器處理泛型有兩種方式:
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 NumericValue
implements 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!由於經過型別擦除,所有的泛型類例項都關聯到同一份位元組碼上,泛型類的所有靜態變數是共享的。
五、總結
虛擬機器中沒有泛型,只有普通類和普通方法,所有泛型類的型別引數在編譯時都會被擦除,泛型類並沒有自己獨有的Class類物件。比如並不存在List<String>.class或是List<Integer>.class,而只有List.class。
建立泛型物件時請指明型別,讓編譯器儘早的做引數檢查( Effective Java,第23條:請不要在新程式碼中使用原生態型別 )
不要忽略編譯器的警告資訊,那意味著潛在的ClassCastException等著你。
靜態變數是被泛型類的所有例項所共享的。對於宣告為MyClass<T>的類,訪問其中的靜態變數的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>建立的物件,都是共享一個靜態變數。
泛型的型別引數不能用在Java異常處理的catch語句中。因為異常處理是由JVM在執行時刻來進行的。由於型別資訊被擦除,JVM是無法區分兩個異常型別MyException<String>和MyException<Integer>的。對於JVM來說,它們都是 MyException型別的。也就無法執行與異常對應的catch語句。
為了讓學習變得輕鬆、高效,今天給大家免費分享一套Java教學資源。幫助大家在成為Java架構師的道路上披荊斬棘。需要資料的歡迎加入學習交流群:9285,05736
