Java泛型:类型擦除
前情回顧
Java泛型:泛型類、泛型接口和泛型方法
類型擦除
代碼片段一
| 1 2 3 4 5 6 7 | Class c1 = new ArrayList<Integer>().getClass(); Class c2 = new ArrayList<String>().getClass(); System.out.println(c1 == c2);/* Output true */ |
顯然在平時使用中,ArrayList<Integer>()和new ArrayList<String>()是完全不同的類型,但是在這里,程序卻的的確確會輸出true。
這就是Java泛型的類型擦除造成的,因為不管是ArrayList<Integer>()還是new ArrayList<String>(),都在編譯器被編譯器擦除成了ArrayList。那編譯器為什么要做這件事?原因也和大多數的Java讓人不爽的點一樣——兼容性。由于泛型并不是從Java誕生就存在的一個特性,而是等到SE5才被加入的,所以為了兼容之前并未使用泛型的類庫和代碼,不得不讓編譯器擦除掉代碼中有關于泛型類型信息的部分,這樣最后生成出來的代碼其實是『泛型無關』的,我們使用別人的代碼或者類庫時也就不需要關心對方代碼是否已經『泛化』,反之亦然。
在編譯器層面做的這件事(擦除具體的類型信息),使得Java的泛型先天都存在一個讓人非常難受的缺點:
在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。
代碼片段二
| 1 2 3 4 5 6 7 8 9 | List<Integer> list = new ArrayList<Integer>(); Map<Integer, String> map = new HashMap<Integer, String>(); System.out.println(Arrays.toString(list.getClass().getTypeParameters())); System.out.println(Arrays.toString(map.getClass().getTypeParameters()));/* Output [E] [K, V] */ |
關于getTypeParameters()的解釋:
Returns an array of TypeVariable objects that represent the type variables declared by the generic declaration represented by this GenericDeclaration object, in declaration order. Returns an array of length 0 if the underlying generic declaration declares no type variables.
我們期待的是得到泛型參數的類型,但是實際上我們只得到了一堆占位符。
代碼片段三
| 1 2 3 4 5 6 7 | public class Main<T> {public T[] makeArray() {// error: Type parameter 'T' cannot be instantiated directlyreturn new T[5];} } |
我們無法在泛型內部創建一個T類型的數組,原因也和之前一樣,T僅僅是個占位符,并沒有真實的類型信息,實際上,除了new表達式之外,instanceof操作和轉型(會收到警告)在泛型內部都是無法使用的,而造成這個的原因就是之前講過的編譯器對類型信息進行了擦除。
同時,面對泛型內部形如T var;的代碼時,記得多念幾遍:它只是個Object,它只是個Object……
代碼片段四
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class Main<T> {private T t;public void set(T t) {this.t = t;}public T get() {return t;}public static void main(String[] args) {Main<String> m = new Main<String>();m.set("findingsea");String s = m.get();System.out.println(s);} }/* Output findingsea */ |
雖然有類型擦除的存在,使得編譯器在泛型內部其實完全無法知道有關T的任何信息,但是編譯器可以保證重要的一點:內部一致性,也是我們放進去的是什么類型的對象,取出來還是相同類型的對象,這一點讓Java的泛型起碼還是有用武之地的。
代碼片段四展現就是編譯器確保了我們放在t上的類型的確是T(即便它并不知道有關T的任何類型信息)。這種確保其實做了兩步工作:
- set()處的類型檢驗
- get()處的類型轉換
這兩步工作也成為邊界動作。
代碼片段五
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class Main<T> {public List<T> fillList(T t, int size) {List<T> list = new ArrayList<T>();for (int i = 0; i < size; i++) {list.add(t);}return list;}public static void main(String[] args) {Main<String> m = new Main<String>();List<String> list = m.fillList("findingsea", 5);System.out.println(list.toString());} }/* Output [findingsea, findingsea, findingsea, findingsea, findingsea] */ |
代碼片段五同樣展示的是泛型的內部一致性。
擦除的補償
如上看到的,但凡是涉及到確切類型信息的操作,在泛型內部都是無法共工作的。那是否有辦法繞過這個問題來編程,答案就是顯示地傳遞類型標簽。
代碼片段六
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Main<T> {public T create(Class<T> type) {try {return type.newInstance();} catch (Exception e) {e.printStackTrace();}return null;}public static void main(String[] args) {Main<String> m = new Main<String>();String s = m.create(String.class);} } |
代碼片段六展示了一種用類型標簽生成新對象的方法,但是這個辦法很脆弱,因為這種辦法要求對應的類型必須有默認構造函數,遇到Integer類型的時候就失敗了,而且這個錯誤還不能在編譯器捕獲。
進階的方法可以用限制類型的顯示工廠和模板方法設計模式來改進這個問題,具體可以參見《Java編程思想 (第4版)》P382。
代碼片段七
| 1 2 3 4 5 6 7 8 9 10 11 | public class Main<T> {public T[] create(Class<T> type) {return (T[]) Array.newInstance(type, 10);}public static void main(String[] args) {Main<String> m = new Main<String>();String[] strings = m.create(String.class);} } |
代碼片段七展示了對泛型數組的擦除補償,本質方法還是通過顯示地傳遞類型標簽,通過Array.newInstance(type, size)來生成數組,同時也是最為推薦的在泛型內部生成數組的方法。
以上,泛型的第二部分的結束。
原文作者:?findingsea
原文鏈接:?http://findingsea.github.io/2015/10/09/java-generic-type-erasure/
總結
以上是生活随笔為你收集整理的Java泛型:类型擦除的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java泛型(二)、泛型的内部原理:类型
- 下一篇: Java语言 泛型 类型擦除