1. 程式人生 > >Java泛型-型別擦除

Java泛型-型別擦除

一、概述

      Java泛型在使用過程有諸多的問題,如不存在List<String>.class, List<Integer>不能賦值給List<Number>(不可協變),奇怪的ClassCastException等。 正確的使用Java泛型需要深入的瞭解Java的一些概念,如協變,橋接方法,以及這篇筆記記錄的型別擦除。Java泛型的處理幾乎都在編譯器中進行,編譯器生成的bytecode是不包涵泛型資訊的,泛型型別資訊將在編譯處理是被擦除,這個過程即型別擦除。

二、編譯器如何處理泛型?

     通常情況下,一個編譯器處理泛型有兩種方式:
     1.Code specialization。在例項化一個泛型類或泛型方法時都產生一份新的目的碼(位元組碼or二進位制程式碼)。例如,針對一個泛型list,可能需要 針對string,integer,float產生三份目的碼。
     2.Code sharing。對每個泛型類只生成唯一的一份目的碼;該泛型類的所有例項都對映到這份目的碼上,在需要的時候執行型別檢查和型別轉換。
     C++中的模板(template)是典型的Code specialization實現。C++編譯器會為每一個泛型類例項生成一份執行程式碼。執行程式碼中integer list和string list是兩種不同的型別。這樣會導致程式碼膨脹(code bloat),不過有經驗的C++程式設計師可以有技巧的避免程式碼膨脹。
     Code specialization另外一個弊端是在引用型別系統中,浪費空間,因為引用型別集合中元素本質上都是一個指標。沒必要為每個型別都產生一份執行程式碼。而這也是Java編譯器中採用Code sharing方式處理泛型的主要原因。
     Java編譯器通過Code sharing方式為每個泛型型別建立唯一的位元組碼錶示,並且將該泛型型別的例項都對映到這個唯一的位元組碼錶示上。將多種泛型類形例項對映到唯一的位元組碼錶示是通過型別擦除(type erasue)實現的。

三、 什麼是型別擦除?

     型別擦除指的是通過型別引數合併,將泛型型別例項關聯到同一份位元組碼上。編譯器只為泛型型別生成一份位元組碼,並將其例項關聯到這份位元組碼上。型別擦除的關鍵在於從泛型型別中清除型別引數的相關資訊,並且再必要的時候新增型別檢查和型別轉換的方法。
     型別擦除可以簡單的理解為將泛型java程式碼轉換為普通java程式碼,只不過編譯器更直接點,將泛型java程式碼直接轉換成普通java位元組碼。
     型別擦除的主要過程如下:
     1.將所有的泛型引數用其最左邊界(最頂級的父型別)型別替換。
     2.移除所有的型別引數。
     如

Java程式碼  收藏程式碼
  1. interface Comparable <A> {   
  2.   public int compareTo( A that);   
  3. }   
  4. final class NumericValue implements Comparable <NumericValue> {   
  5.   priva te byte value;    
  6.   public  NumericValue (byte value) { this.value = value; }    
  7.   public  byte getValue() { return value; }    
  8.   public  int compareTo( NumericValue t hat) { return
     this.value - that.value; }   
  9. }   
  10. -----------------  
  11. class Collections {    
  12.   public static <A extends Comparable<A>>A max(Collection <A> xs) {   
  13.     Iterator <A> xi = xs.iterator();   
  14.     A w = xi.next();   
  15.     while (xi.hasNext()) {   
  16.       A x = xi.next();   
  17.       if (w.compareTo(x) < 0) w = x;   
  18.     }   
  19.     return w;   
  20.   }   
  21. }   
  22. final class Test {   
  23.   public static void main (String[ ] args) {   
  24.     LinkedList <NumericValue> numberList = new LinkedList <NumericValue> ();   
  25.     numberList .add(new NumericValue((byte)0));    
  26.     numberList .add(new NumericValue((byte)1));    
  27.     NumericValue y = Collections.max( numberList );    
  28.   }   
  29. }<span style="color: #333333; font-family: Arial; font-size: 14px;">  
  30. </span>  

經過型別擦除後的型別為
 interface Comparable { 

Java程式碼  收藏程式碼
  1.   public int compareTo( Object that);   
  2. }   
  3. final class NumericValue implements Comparable {   
  4.   priva te byte value;    
  5.   public  NumericValue (byte value) { this.value = value; }    
  6.   public  byte getValue() { return value; }    
  7.   public  int compareTo( NumericValue t hat)   { return this.value - that.value; }   
  8.   public  int compareTo(Object that) { return this.compareTo((NumericValue)that);  }   
  9. }   
  10. -------------  
  11. class Collections {    
  12.   public static Comparable max(Collection xs) {   
  13.     Iterator xi = xs.iterator();   
  14.     Comparable w = (Comparable) xi.next();   
  15.     while (xi.hasNext()) {   
  16.       Comparable x = (Comparable) xi.next();   
  17.       if (w.compareTo(x) < 0) w = x;   
  18.     }   
  19.     return w;   
  20.   }   
  21. }   
  22. final class Test {   
  23.   public static void main (String[ ] args) {   
  24.     LinkedList numberList = new LinkedList();   
  25.     numberList .add(new NumericValue((byte)0));  ,  
  26.     numberList .add(new NumericValue((byte)1));    
  27.     NumericValue y = (NumericValue) Collections.max( numberList );    
  28.   }   
  29. }<span style="color: #333333; font-family: Arial; font-size: 14px;">  
  30. </span>  

第一個泛型類Comparable <A>擦除後 A被替換為最左邊界Object。Comparable<NumericValue>的型別引數NumericValue被擦除掉,但是這直 接導致NumericValue沒有實現介面Comparable的compareTo(Object that)方法,於是編譯器充當好人,添加了一個橋接方法。
第二個示例中限定了型別引數的邊界<A extends Comparable<A>>A,A必須為Comparable<A>的子類,按照型別擦除的過程,先講所有的型別引數 ti換為最左邊界Comparable<A>,然後去掉引數型別A,得到最終的擦除後結果。

四、型別擦除帶來的問題

     正是由於型別擦除的隱蔽存在,直接導致了眾多的泛型靈異問題。
 Q1.用同一泛型類的例項區分方法簽名?——NO!
    import java.util.*;

Java程式碼  收藏程式碼
  1.     public class Erasure{  
  2.             public void test(List<String> ls){  
  3.                 System.out.println("Sting");  
  4.             }  
  5.             public void test(List<Integer> li){  
  6.                 System.out.println("Integer");  
  7.             }  
  8.     }<span style="color: #333333; font-family: Arial; font-size: 14px;">  
  9. </span>  

編譯該類,

 

引數型別明明不一樣啊,一個List<String>,一個是List<Integer>,但是,偷偷的說,type erasure之後,它就都是List了⋯⋯
Q2. 同時catch同一個泛型異常類的多個例項?——NO!
同理,如果定義了一個泛型一場類GenericException<T>,千萬別同時catch GenericException<Integer>和GenericException<String>,因為他們是一樣一樣滴⋯⋯
Q3.泛型類的靜態變數是共享的?——Yes!
猜猜這段程式碼的輸出是什麼?

Java程式碼  收藏程式碼
  1. import java.util.*;  
  2. public class StaticTest{  
  3.     public static void main(String[] args){  
  4.         GT<Integer> gti = new GT<Integer>();  
  5.         gti.var=1;  
  6.         GT<String> gts = new GT<String>();  
  7.         gts.var=2;  
  8.         System.out.println(gti.var);  
  9.     }  
  10. }  
  11. class GT<T>{  
  12.     public static int var=0;  
  13.     public void nothing(T x){}  
  14. }<span style="color: #333333; font-family: Arial; font-size: 14px;">  
  15. </span>  

答案是——2!由於經過型別擦除,所有的泛型類例項都關聯到同一份位元組碼上,泛型類的所有靜態變數是共享的。

五、Just remember

1.虛擬機器中沒有泛型,只有普通類和普通方法
2.所有泛型類的型別引數在編譯時都會被擦除
3.建立泛型物件時請指明型別,讓編譯器儘早的做引數檢查(Effective Java,第23條:請不要在新程式碼中使用原生態型別)
4.不要忽略編譯器的警告資訊,那意味著潛在的ClassCastException等著你。