2.6 Java泛型詳解
Java泛型是JDK5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter),聲明的類型參數在使用時用具體的類型來替換。泛型最主要的應用是在JDK5中的新集合類框架中。
2.6.1 類型擦除
首先我們看一下Java泛型中的類型擦除:在生成的Java字節碼中是不包含泛型的類型信息的,使用泛型的時候加上的類型參數在編譯的時候會被編譯器去掉,這個過程就稱為類型擦除。如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的(反射是可見的,具體可參見2.3中筆記),這一點和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> {
@Override
public int compareTo(String str) {
return 0;
}
}
當類型信息被擦除之后,上述類的聲明變成了class MyString implements Comparable。但是這樣的話,類MyString就會有編譯錯誤,因為沒有實現接口Comparable聲明的int compareTo(Object)方法,這個時候就由編譯器來動態生成這個方法。
2.6.2 通配符與上下界
在使用泛型類的時候,既可以指定一個具體的類型,也可以用通配符?來表示未知類型,如List<?>就聲明了List中包含的元素類型是未知的。通配符所代表的其實是一組類型,但具體的類型是未知的。通配符分為三類:無界通配符、上界通配符和下界通配符。通配符本身比較復雜,我們會以集合為代表,以元素的添加和獲取為例簡要說明其用法。
無界通配符
“?”表示無界通配符,List<?>表示:List中存儲的元素的類型是未知的。
- 添加元素,使用無界通配符時,由于類型不確定的(可以是任何類型),不可以向List<?>添加任何元素(除了null),因為如果它包含了String的話,往里面添加Integer顯然是錯誤的。那為什么不能添加Object引用,是因為Object引用可以指向子類實例,編譯期是無法獲知其具體類型,所以為了類型安全,其不可以添加任何元素(除了null)。
- 獲取元素,List<?>中的元素只可以使用Object來引用,因為其元素肯定是Object及其子類引用。
事實上無界通配符通常會用在以下兩種情況:
- 在業務邏輯與泛型類型無關,如List.size和List.clean等。實際上,最常用的就是Class<?>,因為Class<T>并沒有依賴于T。
- 當方法參數是原始的Object類型,如下:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + "");
}
//使用泛型類替換
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + "");
}
這樣就可以兼容更多的輸出,而不單純是List<Object>,如下:
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
上界通配符
“? extends Animal”表示通配符的上界是Animal,即“? extends Animal”可以代表Animal及其子類,不能代表Animal父類。
首先要闡明一點,上界通配符和下界通配符更多的是為了解決泛型不協變的問題。
- 添加元素,使用上界通配符時,其類型仍然是不確定的(會是某個類型及其子類型),所以仍然不可以向List<? extends Animal>添加任何元素(null除外),因為如果它包含了Cat的話,往里面添加Dog顯然是錯誤的,同樣不可以添加Object引用。
- 獲取元素,List<? extends Animal>中的元素可以用Animal來引用的,因為其元素肯定是Animal及其子類引用。
而且引入了上界之后,在使用類型的時候就可以使用上界類中定義的方法,因為其中元素肯定是Animal類或其子類成員引用。
下界通配符
“? super Animal”表示通配符的下界是Animal,即“? super Animal”可以代表Animal及其父類,不能代表Animal子類。
- 添加元素,使用下界通配符時,雖然類型仍然是不確定的(會是某個類型及其父類),但是此時可以向List<? super Animal>添加Animal或者其子類型Dog元素,因為如果它包含了Animal的話,往里面添加Dog顯然是可以的,子類型可以替換父類型。
- 獲取元素,List<? super Animal>中的元素只可以用Object來引用的,“? super Animal”可以代表Animal及其父類,所以只能通過Object來進行應用。
總結
PECS(Producer Extends Consumer Super)原則:頻繁往外讀取內容的,適合用上界Extends;經常往里插入的,適合用下界Super。
通配符是實參,通配符這些看起來很奇怪的特性原因在于:編譯器要保證類型安全,Object類型是所有類型的祖宗類型,而父類引用是可以引用子類對象的。
2.6.3 類型系統
在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>的子類型。
- 如果泛型類中包含多個類型參數,則對于每個類型參數分別應用上面的規則。
2.6.4 開發自己的泛型類
泛型類與一般的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(); //編譯錯誤,不能創建對象
}
}
2.6.5 最佳實踐
在使用泛型的時候可以遵循一些基本的原則,從而避免一些常見的問題。
- 在代碼中避免泛型類和原始類型的混用。比如List<String>和List不應該共同使用,這樣會產生一些編譯器警告和潛在的運行時異常。
- 在使用帶通配符的泛型類的時候,需要明確通配符所代表的一組類型的概念。由于具體的類型是未知的,很多操作是不允許的。
- 泛型類最好不要同數組一塊使用。你只能創建new List<?>[10]這樣的數組,無法創建new List<String>[10]這樣的。這限制了數組的使用能力,而且會帶來很多費解的問題。因此當需要類似數組的功能時候,使用集合類即可。