Java中泛型 類型擦除

轉自:Java中泛型是類型擦除的html

 

        Java 泛型(Generic)的引入增強了參數類型的安全性,減小了類型的轉換,但有一點須要注意:Java 的泛型在編譯器有效,在運行期被刪除,也就是說全部泛型參數類型在編譯後都會被清除掉,看下面一個列子,代碼以下:java

public class Foo {  
    public void listMethod(List<String> stringList){  
    }  
    public void listMethod(List<Integer> intList) {  
    }  
}  

 

  代碼很簡單,看起來沒什麼問題,可是編譯器卻報出以下錯誤信息:api

        Method listMethod(List<String>) has the same erasure listMethod(List<E>) as another method in type Foo數組

        此錯誤的意思是說listMethod(List<String>) 方法在編譯時擦除類型後的方法是listMethod(List<E>),它與另一個方法重複,也就是方法簽名重複。反編譯以後的方法代碼以下:安全

public void listMethod(List list)  
{  
}

 

   從上面代碼能夠看出 Java 編譯後的字節碼中已經沒有泛型的任何信息,在編譯後全部的泛型類型都會作相應的轉化,轉化以下:

 

  • List<String>、List<T> 擦除後的類型爲 List。
  • List<String>[]、List<T>[] 擦除後的類型爲 List[]。
  • List<? extends E>、List<? super E> 擦除後的類型爲 List<E>。
  • List<T extends Serialzable & Cloneable> 擦除後類型爲 List<Serializable>。

        Java 爲何這麼處理呢?有如下兩個緣由:oracle

  1. 避免 JVM 的大換血。若是 JVM 將泛型類型延續到運行期,那麼到運行期時 JVM 就須要進行大量的重構工做了,提升了運行期的效率。
  2. 版本兼容。 在編譯期擦除能夠更好地支持原生類型(Raw Type)。

        明白了 Java 泛型是類型擦除的,下面的問題就很好理解了:ui

        (1) 泛型的 class 對象是相同的spa

        每一個類都有一個 class 屬性,泛型化不會改變 class 屬性的返回值,例如:.net

public static void main(String[] args) {  
    List<String> ls = new ArrayList<String>();  
    List<Integer> li = new ArrayList<Integer>();  
    System.out.println(ls.getClass() == li.getClass());  
}  

  代碼返回值爲 true,緣由很簡單,List<String> 和 List<Integer> 擦除後的類型都是 List。code

        (2) 泛型數組初始化時不能聲明泛型類型

        以下代碼編譯時通不過:

List<String>[] list = new List<String>[];  

  在這裏能夠聲明一個帶有泛型參數的數組,可是不能初始化該數組,由於執行了類型擦除操做後,List<Object>[] 與 List<String>[] 就是同一回事了,編譯器拒絕如此聲明。

        (3) instanceof 不容許存在泛型參數

        如下代碼不能經過編譯,緣由同樣,泛型類型被擦除了。

List<String> list = new ArrayList<String>();  
System.out.println(list instanceof List<String>)

錯誤信息以下:

Cannot perform instanceof check against parameterized type List<String>. Use the form List<?> instead since further generic type information will be erased at runtime

 

 

如下轉自:Java泛型:類型檫除、模板和泛型傳遞

 

類型擦除

正確理解泛型概念的首要前提是理解類型擦除(type erasure)。 Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。如在代碼中定義的List<Object>和List<String>等類型,在編譯以後都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來講是不可見的。Java編譯器會在編譯時儘量的發現可能出錯的地方,可是仍然沒法避免在運行時刻出現類型轉換異常的狀況。類型擦除也是Java的泛型實現方式與C++模板機制實現方式之間的重要區別。

不少泛型的奇怪特性都與這個類型擦除的存在有關,包括:

  • 泛型類並無本身獨有的Class類對象。好比並不存在List<String>.class或是List<Integer>.class,而只有List.class。
  • 靜態變量是被泛型類的全部實例所共享的。對於聲明爲MyClass<T>的類,訪問其中的靜態變量的方法仍然是 MyClass.myStaticVar。不論是經過new MyClass<String>仍是new MyClass<Integer>建立的對象,都是共享一個靜態變量。
  • 泛型的類型參數不能用在Java異常處理的catch語句中。由於異常處理是由JVM在運行時刻來進行的。因爲類型信息被擦除,JVM是沒法區分兩個異常類型MyException<String>和MyException<Integer>的。對於JVM來講,它們都是 MyException類型的。也就沒法執行與異常對應的catch語句。

類型擦除的基本過程也比較簡單,首先是找到用來替換類型參數的具體類。這個具體類通常是Object。若是指定了類型參數的上界的話,則使用這個上界。把代碼中的類型參數都替換成具體的類。同時去掉出現的類型聲明,即去掉<>的內容。好比T get()方法聲明就變成了Object get();List<String>就變成了List。接下來就可能須要生成一些橋接方法(bridge method)。這是因爲擦除了類型以後的類可能缺乏某些必須的方法。好比考慮下面的代碼:

class MyString implements Comparable<String> {
    public int compareTo(String str) {        
        return 0;    
    }
} 

 

當類型信息被擦除以後,上述類的聲明變成了class MyString implements Comparable。可是這樣的話,類MyString就會有編譯錯誤,由於沒有實現接口Comparable聲明的int compareTo(Object)方法。這個時候就由編譯器來動態生成這個方法。

實例分析

瞭解了類型擦除機制以後,就會明白編譯器承擔了所有的類型檢查工做。編譯器禁止某些泛型的使用方式,正是爲了確保類型的安全性。以上面提到的List<Object>和List<String>爲例來具體分析:

public void inspect(List<Object> list) {    
    for (Object obj : list) {        
        System.out.println(obj);    
    }    
    list.add(1); //這個操做在當前方法的上下文是合法的。 
}
public void test() {    
    List<String> strs = new ArrayList<String>();    
    inspect(strs); //編譯錯誤 
}

 

這段代碼中,inspect方法接受List<Object>做爲參數,當在test方法中試圖傳入List<String>的時候,會出現編譯錯誤。假設這樣的作法是容許的,那麼在inspect方法就能夠經過list.add(1)來向集合中添加一個數字。這樣在test方法看來,其聲明爲List<String>的集合中卻被添加了一個Integer類型的對象。這顯然是違反類型安全的原則的,在某個時候確定會拋出ClassCastException。所以,編譯器禁止這樣的行爲。編譯器會盡量的檢查可能存在的類型安全問題。對於肯定是違反相關原則的地方,會給出編譯錯誤。當編譯器沒法判斷類型的使用是否正確的時候,會給出警告信息。

通配符與上下界

在使用泛型類的時候,既能夠指定一個具體的類型,如List<String>就聲明瞭具體的類型是String;也能夠用通配符?來表示未知類型,如List<?>就聲明瞭List中包含的元素類型是未知的。 通配符所表明的實際上是一組類型,但具體的類型是未知的。List<?>所聲明的就是全部類型都是能夠的。可是List<?>並不等同於List<Object>。List<Object>實際上肯定了List中包含的是Object及其子類,在使用的時候均可以經過Object來進行引用。而List<?>則其中所包含的元素類型是不肯定。其中可能包含的是String,也多是 Integer。若是它包含了String的話,往裏面添加Integer類型的元素就是錯誤的。正由於類型未知,就不能經過new ArrayList<?>()的方法來建立一個新的ArrayList對象。由於編譯器沒法知道具體的類型是什麼。可是對於 List<?>中的元素確老是能夠用Object來引用的,由於雖然類型未知,但確定是Object及其子類。考慮下面的代碼:

public void wildcard(List<?> list) {
    list.add(1);//編譯錯誤 
} 

 

如上所示,試圖對一個帶通配符的泛型類進行操做的時候,老是會出現編譯錯誤。其緣由在於通配符所表示的類型是未知的。

由於對於List<?>中的元素只能用Object來引用,在有些狀況下不是很方便。在這些狀況下,可使用上下界來限制未知類型的範圍。 如List<? extends Number>說明List中可能包含的元素類型是Number及其子類。而List<? super Number>則說明List中包含的是Number及其父類。當引入了上界以後,在使用類型的時候就可使用上界類中定義的方法。好比訪問 List<? extends Number>的時候,就可使用Number類的intValue等方法。

類型系統

在Java中,你們比較熟悉的是經過繼承機制而產生的類型體系結構。好比String繼承自Object。根據Liskov替換原則,子類是能夠替換父類的。當須要Object類的引用的時候,若是傳入一個String對象是沒有任何問題的。可是反過來的話,即用父類的引用替換子類引用的時候,就須要進行強制類型轉換。編譯器並不能保證運行時刻這種轉換必定是合法的。這種自動的子類替換父類的類型轉換機制,對於數組也是適用的。 String[]能夠替換Object[]。可是泛型的引入,對於這個類型系統產生了必定的影響。正如前面提到的List<String>是不能替換掉List<Object>的。

引入泛型以後的類型系統增長了兩個維度:一個是類型參數自身的繼承體系結構,另一個是泛型類或接口自身的繼承體系結構。第一個指的是對於 List<String>和List<Object>這樣的狀況,類型參數String是繼承自Object的。而第二種指的是 List接口繼承自Collection接口。對於這個類型系統,有以下的一些規則:

  • 相同類型參數的泛型類的關係取決於泛型類自身的繼承體系結構。即List<String>是Collection<String> 的子類型,List<String>能夠替換Collection<String>。這種狀況也適用於帶有上下界的類型聲明。
  • 當泛型類的類型聲明中使用了通配符的時候, 其子類型能夠在兩個維度上分別展開。如對Collection<? extends Number>來講,其子類型能夠在Collection這個維度上展開,即List<? extends Number>和Set<? extends Number>等;也能夠在Number這個層次上展開,即Collection<Double>和 Collection<Integer>等。如此循環下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子類型。
  • 若是泛型類中包含多個類型參數,則對於每一個類型參數分別應用上面的規則。

理解了上面的規則以後,就能夠很容易的修正實例分析中給出的代碼了。只須要把List<Object>改爲List<?>便可。List<String>是List<?>的子類型,所以傳遞參數時不會發生錯誤。

開發本身的泛型類

泛型類與通常的Java類基本相同,只是在類和接口定義上多出來了用<>聲明的類型參數。一個類能夠有多個類型參數,如 MyClass<X, Y, Z>。 每一個類型參數在聲明的時候能夠指定上界。所聲明的類型參數在Java類中能夠像通常的類型同樣做爲方法的參數和返回值,或是做爲域和局部變量的類型。可是因爲類型擦除機制,類型參數並不能用來建立對象或是做爲靜態變量的類型。考慮下面的泛型類中的正確和錯誤的用法。

class ClassTest<X extends Number, Y, Z> {    
    private X x;    
    private static Y y; //編譯錯誤,不能用在靜態變量中    
    public X getFirst() {
        //正確用法        
        return x;    
    }    
    public void wrong() {        
        Z z = new Z(); //編譯錯誤,不能建立對象    
    }
} 

 

泛型傳遞

即泛型能夠看成參數在不一樣的實例化的類中傳遞,理論上來講能夠無限制層次的傳遞下去。最終會約束每一層的方法或者類型的泛型肯定,在《泛型傳遞》這篇文章中對具體的用法進行詳盡的描述。

最佳實踐

在使用泛型的時候能夠遵循一些基本的原則,從而避免一些常見的問題。

  • 在代碼中避免泛型類和原始類型的混用。好比List<String>和List不該該共同使用。這樣會產生一些編譯器警告和潛在的運行時異常。當須要利用JDK 5以前開發的遺留代碼,而不得不這麼作時,也儘量的隔離相關的代碼。
  • 在使用帶通配符的泛型類的時候,須要明確通配符所表明的一組類型的概念。因爲具體的類型是未知的,不少操做是不容許的。
  • 泛型類最好不要同數組一塊使用。你只能建立new List<?>[10]這樣的數組,沒法建立new List<String>[10]這樣的。這限制了數組的使用能力,並且會帶來不少費解的問題。所以,當須要相似數組的功能時候,使用集合類便可。
  • 不要忽視編譯器給出的警告信息。
相關文章
相關標籤/搜索