聊聊Java的泛型及實現

前言

和C++以模板來實現靜多態不一樣,Java基於運行時支持選擇了泛型,二者的實現原理大相庭徑。C++能夠支持基本類型做爲模板參數,Java卻只能接受類做爲泛型參數;Java能夠在泛型類的方法中取得本身泛型參數的Class類型,C++只能由編譯器推斷在鮮爲人知的地方生成新的類,對於特定的模板參數你只能使用特化。在本文中我主要想聊聊泛型的實現原理和一些高級特性。html

泛型基礎

泛型是對Java語言類型系統的一種擴展,有點相似於C++的模板,能夠把類型參數看做是使用參數化類型時指定的類型的一個佔位符。引入泛型,是對Java語言一個較大的功能加強,帶來了不少的好處:java

  1. 類型安全。類型錯誤如今在編譯期間就被捕獲到了,而不是在運行時看成java.lang.ClassCastException展現出來,將類型檢查從運行時挪到編譯時有助於開發者更容易找到錯誤,並提升程序的可靠性數組

  2. 消除了代碼中許多的強制類型轉換,加強了代碼的可讀性安全

  3. 爲較大的優化帶來了可能ide

泛型是什麼並不會對一個對象實例是什麼類型的形成影響,因此,經過改變泛型的方式試圖定義不一樣的重載方法是不能夠的。剩下的內容我不會對泛型的使用作過多的講述,泛型的通配符等知識請自行查閱。函數

在進入下面的論述以前我想先問幾個問題:優化

  • 定義一個泛型類最後到底會生成幾個類,好比ArrayList<T>到底有幾個類.net

  • 定義一個泛型方法最終會有幾個方法在class文件中code

  • 爲何泛型參數不能是基本類型呢htm

  • ArrayList<Integer>是一個類嗎

  • ArrayList<Integer>和List<Integer>和ArrayList<Number>和List<Number>是什麼關係呢,這幾個類型的引用能相互賦值嗎

類型擦除

正確理解泛型概念的首要前提是理解類型擦除(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。

泛型的實現原理

由於種種緣由,Java不能實現真正的泛型,只能使用類型擦除來實現僞泛型,這樣雖然不會有類型膨脹(C++模板使人困擾的難題)的問題,可是也引發了許多新的問題。因此,Sun對這些問題做出了許多限制,避免咱們犯各類錯誤。

保證類型安全

首先第一個是泛型所宣稱的類型安全,既然類型擦除了,如何保證咱們只能使用泛型變量限定的類型呢?java編譯器是經過先檢查代碼中泛型的類型,而後再進行類型擦除,在進行編譯的。那類型檢查是針對誰的呢,讓咱們先看一個例子。

ArrayList<String> arrayList1=new ArrayList(); // 正確,只能放入String
ArrayList arrayList2=new ArrayList<String>(); // 能夠放入任意Object

這樣是沒有錯誤的,不過會有個編譯時警告。不過在第一種狀況,能夠實現與 徹底使用泛型參數同樣的效果,第二種則徹底沒效果。由於,原本類型檢查就是編譯時完成的。new ArrayList()只是在內存中開闢一個存儲空間,能夠存儲任何的類型對象。而真正涉及類型檢查的是它的引用,由於咱們是使用它引用arrayList1 來調用它的方法,好比說調用add()方法。因此arrayList1引用能完成泛型類型的檢查。
而引用arrayList2沒有使用泛型,因此不行。

類型檢查就是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。

實現自動類型轉換

由於類型擦除的問題,因此全部的泛型類型變量最後都會被替換爲原始類型。這樣就引發了一個問題,既然都被替換爲原始類型,那麼爲何咱們在獲取的時候,不須要進行強制類型轉換呢?

public class Test {  
    public static void main(String[] args) {  
        ArrayList<Date> list=new ArrayList<Date>();  
        list.add(new Date());  
        Date myDate=list.get(0);
    }      
}

編譯器生成的class文件中會在你調用泛型方法完成以後返回調用點以前加上類型轉換的操做,好比上文的get函數,就是在get方法完成後,jump回本來的賦值操做的指令位置以前加入了強制轉換,轉換的類型由編譯器推導。

泛型中的繼承關係

先看一個例子:

class DateInter extends A<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

先來分析setValue方法,父類的類型是Object,而子類的類型是Date,參數類型不同,這若是實在普通的繼承關係中,根本就不會是重寫,而是重載。

public void setValue(java.util.Date);  //咱們重寫的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                // invoke A setValue
:(Ljava/lang/Object;)V  
       5: return  
  
  public java.util.Date getValue();    //咱們重寫的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // A.getValue  
:()Ljava/lang/Object;  
       4: checkcast     #26               
       7: areturn  
  
  public java.lang.Object getValue();     //編譯時由編譯器生成的方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:() 去調用咱們重寫的getValue方法  
;  
       4: areturn  
  
  public void setValue(java.lang.Object);   //編譯時由編譯器生成的方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 
       5: invokevirtual #30                 // Method setValue;   去調用咱們重寫的setValue方法  
)V  
       8: return

而且,還有一點也許會有疑問,子類中的方法 Object getValue()和Date getValue()是同 時存在的,但是若是是常規的兩個方法,他們的方法簽名是同樣的,也就是說虛擬機根本不能分別這兩個方法。若是是咱們本身編寫Java代碼,這樣的代碼是沒法經過編譯器的檢查的,可是虛擬機倒是容許這樣作的,由於虛擬機經過參數類型和返回類型來肯定一個方法,因此編譯器爲了實現泛型的多態容許本身作這個看起來「不合法」的事情,而後交給虛擬器去區別。

咱們再看一個常常出現的例子。

class A {
    Object get(){
        return new Object();
    }
}

class B extends A {
    @Override
    Integer get() {
        return new Integer(1);
    }
}

  public static void main(String[] args){
    A a = new B();
    B b = (B) a;
    A c = new A();
    a.get();
    b.get();
    c.get();
  }

反編譯以後的結果

17: invokespecial #5                  // Method com/suemi/network/test/A."<init>":()V
      20: astore_3
      21: aload_1
      22: invokevirtual #6                  // Method com/suemi/network/test/A.get:()Ljava/lang/Object;
      25: pop
      26: aload_2
      27: invokevirtual #7                  // Method com/suemi/network/test/B.get:()Ljava/lang/Integer;
      30: pop
      31: aload_3
      32: invokevirtual #6                  // Method com/suemi/network/test/A.get:()Ljava/lang/Object;

實際上當咱們使用父類引用調用子類的get時,先調用的是JVM生成的那個覆蓋方法,在橋接方法再調用本身寫的方法實現。

泛型參數的繼承關係

在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<?>的子類型,所以傳遞參數時不會發生錯誤。

我的認爲這裏對上面這種情形使用子類型這種說法來形容這種關係是不當的,由於List<String>等本質上來講不能算做類型,只是對List類型加上了編譯器檢查約束,也就不存在子類型這種說法。只能用是否在賦值時可以進行類型轉換來講明。

泛型使用中的注意點

運行時型別查詢

// 錯誤,爲類型擦除以後,ArrayList<String>只剩下原始類型,泛型信息String不存在了,沒法進行判斷
if( arrayList instanceof ArrayList<String>) 

if( arrayList instanceof ArrayList<?>)    // 正確

異常中使用泛型的問題

  • 不能拋出也不能捕獲泛型類的對象。事實上,泛型類擴展Throwable都不合法。爲何不能擴展Throwable,由於異常都是在運行時捕獲和拋出的,而在編譯的時候,泛型信息全都會被擦除掉。類型信息被擦除後,那麼多個使用不一樣泛型參數地方的catch都變爲原始類型Object,那麼也就是說,多個地方的catch變的如出一轍,這天然不被容許。

  • 不能再catch子句中使用泛型變量。

public static <T extends Throwable> void doWork(Class<T> t){  
        try{  
            ...  
        }catch(T e){ //編譯錯誤  T->Throwable,下面的永遠不會被捕獲,因此不被容許
            ...  
        }catch(IndexOutOfBounds e){  
        }                           
 }

不容許建立泛型類數組

Pair<String,Integer>[] table = new Pair<String,Integer>[10];// 編譯錯誤
Pair[] table = new Pair[10];// 無編譯錯誤

因爲數組必須攜帶本身元素的類型信息,在類型擦除以後,Pair<String,Integer>數組就變成了Pair<Object,Object>數組,數組只能攜帶它的元素是Pair這樣的信息,可是並不能攜帶其泛型參數類型的信息,因此也就沒法保證table[i]賦值的類型安全。編譯器只能禁用這種操做。

泛型類中的靜態方法和靜態變量

泛型類中的靜態方法和靜態變量不可使用泛型類所聲明的泛型類型參數。

public class Test2<T> {    
    public static T one;   //編譯錯誤    
    public static  T show(T one){ //編譯錯誤    
        return null;    
    }    
}

由於泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不須要使用對象來調用。對象都沒有建立,如何肯定這個泛型參數是何種類型,因此固然是錯誤的。

類型擦除後的衝突

class Pair<T>   {  
    public boolean equals(T value) {  
        return null;  
    }        
}

方法重定義了,同時存在兩個equals(Object o)。

參考文章

相關文章
相關標籤/搜索