(轉)泛型

Java泛型(generics)是JDK 5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter)。

 

類型擦除

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

 

通配符與上下界

在使用泛型類的時候,既可以指定一個具體的類型,如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及其子類。

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]這樣的。這限制了數組的使用能力,而且會帶來很多費解的問題。因此,當需要類似數組的功能時候,使用集合類即可。
  • 不要忽視編譯器給出的警告信息。

 

發佈了56 篇原創文章 · 獲贊 0 · 訪問量 1476
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章