Java泛型-類型擦除

1、概述

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

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

     一般狀況下,一個編譯器處理泛型有兩種方式:
     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)實現的。程序員

3、 什麼是類型擦除?

     類型擦除指的是經過類型參數合併,將泛型類型實例關聯到同一份字節碼上。編譯器只爲泛型類型生成一份字節碼,並將其實例關聯到這份字節碼上。類型擦除的關鍵在於從泛型類型中清除類型參數的相關信息,而且再必要的時候添加類型檢查和類型轉換的方法。
     類型擦除能夠簡單的理解爲將泛型java代碼轉換爲普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通java字節碼。
     類型擦除的主要過程以下:
     1.將全部的泛型參數用其最左邊界(最頂級的父類型)類型替換。
     2.移除全部的類型參數。
     如安全

interface Comparable <A> { 
  public int compareTo( A that); 
} 
final class NumericValue implements Comparable <NumericValue> { 
  priva te byte value;  
  public  NumericValue (byte value) { this.value = value; }  
  public  byte getValue() { return value; }  
  public  int compareTo( NumericValue t hat) { return this.value - that.value; } 
} 
-----------------
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; 
  } 
} 
final class Test { 
  public static void main (String[ ] args) { 
    LinkedList <NumericValue> numberList = new LinkedList <NumericValue> (); 
    numberList .add(new NumericValue((byte)0));  
    numberList .add(new NumericValue((byte)1));  
    NumericValue y = Collections.max( numberList );  
  } 
}

通過類型擦除後的類型爲jvm

interface Comparable {
  public int compareTo( Object that); 
} 
final class NumericValue implements Comparable { 
  priva te byte value;  
  public  NumericValue (byte value) { this.value = value; }  
  public  byte getValue() { return value; }  
  public  int compareTo( NumericValue t hat)   { return this.value - that.value; } 
  public  int compareTo(Object that) { return this.compareTo((NumericValue)that);  } 
} 
-------------
class 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; 
  } 
} 
final class Test { 
  public static void main (String[ ] args) { 
    LinkedList numberList = new LinkedList(); 
    numberList .add(new NumericValue((byte)0));  ,
    numberList .add(new NumericValue((byte)1));  
    NumericValue y = (NumericValue) Collections.max( numberList );  
  } 
}


第一個泛型類Comparable <A>擦除後 A被替換爲最左邊界Object。Comparable<NumericValue>的類型參數NumericValue被擦除掉,可是這直 接致使NumericValue沒有實現接口Comparable的compareTo(Object that)方法,因而編譯器充當好人,添加了一個橋接方法。
第二個示例中限定了類型參數的邊界<A extends Comparable<A>>A,A必須爲Comparable<A>的子類,按照類型擦除的過程,先講全部的類型參數 ti換爲最左邊界Comparable<A>,而後去掉參數類型A,獲得最終的擦除後結果。this

4、類型擦除帶來的問題

     正是因爲類型擦除的隱蔽存在,直接致使了衆多的泛型靈異問題。
 Q1.用同一泛型類的不一樣實例區分方法簽名?——NO!編碼

     public class Erasure{

            public void test(List<String> ls){
                System.out.println("Sting");
            }
            public void test(List<Integer> li){
                System.out.println("Integer");
            }
    }

編譯器會報錯,由於類型擦除後jvm就區分不了這個方法了
spa

Q2. 同時catch同一個泛型異常類的多個實例?——NO!
同理,若是定義了一個泛型一場類GenericException<T>,千萬別同時catch GenericException<Integer>和GenericException<String>,指針

由於對於jvm來講他們是同樣的。
Q3.
泛型類的靜態變量是共享的?——Yes!
猜猜這段代碼的輸出是什麼?code

import java.util.*;

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!因爲通過類型擦除,全部的泛型類實例都關聯到同一份字節碼上,泛型類的全部靜態變量是共享orm

的。


Q4.泛型之間不存在繼承關係

方法f(List<Object>)不能接收類型爲List<String>的參數。

能夠再抽象出一箇中間層( <?   extends  Parent > )來解決 方法f(List<? extends Object>)就能夠接收List<String>

參數了。

5、Just remember

1.虛擬機中沒有泛型,只有普通類和普通方法
2.全部泛型類的類型參數在編譯時都會被擦除
3.不要忽略編譯器的警告信息,那意味着潛在的ClassCastException等着你

6、編碼規則

Effective Java,第23條:請不要在新代碼中使用原生態類型

聲明中具備一個或者多個類型參數(type parameter)的類或者接口,就是泛型( generic)類或者接口。

泛型類和泛型接口統稱爲泛型( generic type)。

每種泛型均可以定義一組參數化的類型(parameterized type),構成格式:類(接口)名<參數類型>。

每種泛型都定義一個原生態類型(raw type),即不帶任何實際類型參數的泛型名稱。

若是使用原生態類型,就失掉了泛型在安全性和表述性方面的全部優點

出錯以後應該儘快發現,最好是在編譯時就發現。

一、使用泛型後,能夠很快的發現錯誤。

二、使用泛型後,不須要進行手工轉換了。以下代碼:

若是不提供參數類型,使用集合類型和其餘泛型也仍然是合法的,但不該該這麼作。若是使用原生態類型,就失掉了泛型在安全性和表述性方面的全部優點

原生態類型List和參數化類型List<Object>之間有什麼區別?

前者逃避了泛型檢查,後者則明確告知編譯器,它可以持有任意類型的對象。

你能夠將List<String>傳遞給類型List的參數 卻不能傳遞給類型List<Object>的參數。由於,泛型有子類型化的規則,List<String>是原生態類型List的一個子類型,而不是參數化類型List<Object>的子類型(見第25條)。

若是使用像List這樣的原生類型,就會失掉類型安全性,可是若是使用像List<Object>這樣的參數化類型,則不會。

那若是遇到不肯定元素類型的時候該怎麼辦?

這種狀況是可使用原生態類型,可是很危險。在Java1.5開始,提供了一個安全的替代方法,無限制的通配符(unbounded wildcard type)。如:Set<?>。

有兩種狀況可使用原生態類型。

一、在類文字(class literal)中必須使用原生態類型。也就是說List.class,String[].class和int.class是合法的,可是List<String>.class和List<?>.class是非法的。

二、與instanceof操做符有關。因爲泛型信息能夠在運行時被擦除,所以在參數化類型而非無限制通配符類型上使用instanceof操做符是非法的。

下面是利用泛型來使用instanceof操做符的首選方法:

if(o instanceof Set){

Set<?> m=(Set<?>)o;

}

注意:一旦肯定這個o是個Set,就必須將它轉換成通配符類型Set<?>再用Set<?>作後續操做。由於不加?就能夠add,若是add進去的對象的類型不是否是當初定義的類型別人來用的類型的時候就會有安全性問題。

檢討的?暫時瞭解的不透徹等有機會用的時候再瞭解吧。具體用的再理解吧,像這種應用類型的知識沒有場景不能憑空想一想,而像一些基礎的只是沒有場景也能學會應爲遇到到太多的類似的東西了那個概念太熟了,熟了或者基礎知識的體系太成熟了有點像數學上的公理不須要場景。

相關文章
相關標籤/搜索