Java泛型总结
Java泛型總結
Java泛型是JDK5引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter)。聲明的類型參數在使用的時候使用具體的類型來替換。泛型最主要的應用是在JDK5中的新集合類框架中。對于泛型概念的引入,開發社區的觀點是褒貶不一。從好的方面上說,泛型的引入可以解決之前的集合類框架在使用過程中通常會出現的運行時刻類型錯誤,因為編譯器可以在編譯時刻就發現很多明顯的錯誤。從不好的方面說,為了保證與舊版本的兼容性,Java泛型的實現上還存在著不夠優雅的地方。
類型擦除
正確理解泛型概念的首要前提是理解類型擦除(type erasure)。Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。這個過程就稱為類型擦除。比如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時盡可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。
通過如下代碼片段感受類型擦除:
ArrayList<String> a1 = new ArrayList<>();ArrayList<Integer> a2 =new ArrayList<>();Class c1 =a1.getClass();Class c2 = a2.getClass();System.out.println(c1.equals(c2)); //Output: true此時,程序輸出true,這就是類型擦除造成的。因為不管是ArrayList<String>還是ArrayList<Integer>,都會在編譯期被編譯器擦除成ArrayList。編譯器這么做的原因歸根結底還是為了兼容JDK5前未使用泛型的代碼,因此不得不讓編譯器擦除有關類型信息的部分,這樣生成的代碼其實就是類型無關的。
List<Integer> list = new ArrayList<>();Map<String,Integer> map = new HashMap<>();System.out.println(Arrays.toString(list.getClass().getTypeParameters())); //[E]System.out.println(Arrays.toString(map.getClass().getTypeParameters())); //[K, V]我們期望的是返回泛型參數的類型,結果返回的僅僅是參數的占位符。
public static <T> T[] makeArray(){return new T[10]; //編譯期報錯:不能創建泛型類型的數組}因為T僅僅是個占位符,并不具有真實的類型信息。為了解決這個問題,可以利用反射:
public static <T> T[] makeArray(Class<T> clazz) {return (T[]) Array.newInstance(clazz, 10);}很多泛型的奇怪特性都與類型擦除的存在有關,包括:
泛型類并沒有自己獨有的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聲明的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(); //編譯錯誤,不能查創建對象} }假設允許類型參數聲明為靜態屬性,那么如下代碼將會非常混亂。
public class Computer<T> {private static T os;public Computer(T os) {this.os = os;}public T getOS() {return os;}public static void main(String [] args) {Computer<Linux> c1 = new Computer<>();Computer<MacOS> c2 = new Computer<>();Computer<Windows> c3 = new Computer<>();System.out.println(c1.getOS());System.out.println(c2.getOS());System.out.println(c3.getOS());} }因為os為Computer類的靜態屬性,所以c1,c2,c3這3個Computer實例共享這個屬性,那么此時os的類型是什么?因此,不允許聲明靜態的類型參數屬性。
總結
在使用Java泛型的時候可以遵循一些基本的原則,從而避免一些常見的問題。
在代碼中避免泛型類和原始類型的混用。比如List<String>和List不應該共同使用。這樣會產生一些編譯器警告和潛在的運行時異常。
在使用帶通配符的泛型類的時候,需要明確通配符所代表的一組類型的概念。由于具體的類型是未知的,很多操作是不允許的。
泛型類最好不要同數組一塊兒使用。只能創建new List<?>[10]這樣的數組,無法創建new List<String>[10]這樣的。這限制了數組的使用能力,而且會帶來很多費解的問題。
參考
InfoQ
Java泛型:類型擦除
總結
- 上一篇: phpmyadmin的安装部署
- 下一篇: Java8 (1)