一、引言
復習javac的編譯過程中的解語法糖的時候看見了泛型擦除中的舉例,網上的資料大多比較散各針對性不一,在此做出自己的一些詳細且易懂的總結。
二、泛型簡介
泛型是JDK 1.5的一項新特性,一種編譯器使用的范式,語法糖的一種,能保證類型安全。【注意:繼承中,子類泛型數必須不少於父類泛型數】
為了方便理解,我將泛型分為普通泛型和通配泛型
三、泛型分類
1、普通泛型
就是沒有設置通配的泛型,泛型表示為某一個類。
聲明時: class Test<T>{...}
使用時: Test<Integer> test = new Test<Integer>();
作為無界泛型,其實就是約束左右泛型必須一致。
2、通配泛型
通配泛型包括兩種,無界通配和有界通配。
【無界通配符】
<?>通配符——表示所有類型都能與它匹配
【有界通配符】
extends(上界)通配符——聲明了類型的上界,表示參數化的類型可能是所指定的類型,或者是此類型的子類;
super(下界)通配符——聲明了類型的下界,表示參數化的類型可能是所指定的類型,或者是此類型的父類型,直至Object。類型擦除后剩下(下面以extends舉例)
//有界泛型類型語法 - 繼承自某父類 <T extends ClassA> //有界泛型類型語法 - 實現某接口 <T extends InterfaceB> //有界泛型類型語法 - 多重邊界 <T extends ClassA & InterfaceB & InterfaceC ... > //示例 <N extends Number> //N標識一個泛型類型,其類型只能是Number抽象類的子類 <T extends Number & Comparable & Map> //T標識一個泛型類型,其類型只能是Person類型的子類,並且實現了Comparable 和Map接口
【注意:多重邊界里,只允許第一個能為類,后續必須為接口】
四、List<T> 和 List<?> 和 List<Object> 的區別
類聲明的時候采用List<T>,此時T可以為任何字母,都指代普通泛型。
例如: class Test<T>{...}
實例化的時候采用List<?>,此時?可以為任何類,表示只能存入此類對象,也可以就寫‘<?>’,代表可以存入任何類對象,屬於通配泛型。
例如:List<?> listOfString = new ArrayList<String>;
但是注意List<?>與List<Object>不一樣,前者是所有泛型的通配符,即所有泛型的引用都能與他進行匹配(作為實例化的右邊),而Object只是一個單獨的類,當為實例化左邊的時候,有且僅有為<Object>相匹配(或者不寫泛型),例如:List<Object> list = new ArrayList<Object>();,實例化左邊的泛型作為“答案范圍”,實例化右邊的泛型只能為“答案”是某一個類。例如:
List<String> list = new ArrayList<?>(); // 編譯錯誤:通配符是“答案范圍”不能作為“答案”出現在實例化的右邊 List<?> list = new ArrayList<String>(); // String與?匹配成功 List<? extends Number> list = new ArrayList<? extends Integer>(); // 編譯錯誤:有界泛型同樣也是“答案范圍”,不能出現在實例化的右邊 List<? extends Number> list = new ArrayList<Integer>(); // 右邊的"答案"與左邊的“答案范圍”匹配成功
五、泛型擦除
由來:一開始java並沒有泛型,后來1.5加入了泛型,為了能向前兼容(舊版本的jvm能解釋運行新版本的.class文件)所以就采用了偽泛型——“泛型擦除”,並一直保留了下來。
原理:泛型信息只存在於代碼編譯階段,在進入 JVM 之前,與泛型相關的信息會被擦除掉,擦除后會變成原始類型(去掉<T>,將方法內的T擦除成Object)例如Generic<T>會被擦除成Generic。還需要注意的是,不同的通配符的擦除的方式也有不同:
口訣:【存入:取下界;取出:取上界】—or—【存下,取上】
當泛型作為方法的傳入參數的時候,此時替換成通配泛型的下界,例如add方法
當泛型作為方法的返回參數的時候,此時替換成通配泛型的上界,例如get方法
List<? extends Integer> list1 = new ArrayList<Integer>(); list1.add(null); // 此時傳入取<? extends Integer> 下界————無 所以只能傳null,否則報錯 Integer integer1 = list1.get(0); // // 此時返回取<? extends Integer> 上界————Integer List<? super Integer> list2 = new ArrayList<Integer>(); list2.add(111); // 此時傳入取<? super Integer> 下界——————Integer Integer integer2 = (Integer) list2.get(0); // // 此時返回取<? super Integer> 上界————Object
所以同理可得,當泛型為<?>的時候,取下界是null,取上界是Object。
所以得出結論,因為add和get方法的擦除的限制,盡量少使用通配泛型
泛型擦除有什么隱患,有什么解決方法:
1、如果不加泛型繼承,擦除后會變成原始類型,所以能加入非泛型的類型。 List<Integer> list = new ArrayList<Integer>(); list.add("呀哈");
並且能完成不同泛型之間的引用傳遞。 List<String> list = new ArrayList<Integer>();
以上兩種情況怎么解決?
——java編譯器是通過先檢查代碼中泛型的類型,如果出現上面兩種情況則會在編譯期報錯,檢查通過后,然后再進行類型擦除,再進行編譯。
【注意:先檢查實例化左右泛型是否匹配,然后以實例化的左邊的泛型為基准對添加元素進行檢查(所以,只在右邊寫泛型和沒寫是一個意思)】例如:
List<String> list1 = new ArrayList<String>(); // 此時按照String檢查泛型 List list2 = new ArrayList<String>(); // 此時不檢查泛型
2、擦除后泛型信息就沒了,獲取的時候再強轉?
——泛型補償:在泛型檢查的保證下,存入的都是符合泛型的對象,編譯期間利用反射獲取元素對象的類型(getClass()方法)對要傳出元素進行強轉。
3、子類繼承泛型方法,然后對其重寫並將泛型改成真實類型,但是在擦除之后原來父類的泛型方法會變成Object方法,變為兩個不同的方法,這樣一來此方法就不是繼承重寫,而是子類的重載了。如下面代碼所示:
class Node<T> { public void setData(T data) { System.out.println("Node.setData"); } } class MyNode<T> extends Node<Integer> { public void setData(Integer data) { System.out.println("MyNode.setData:"+data); } }
Node<Integer> n = new MyNode<Integer>(); n.setData(1213); // 如果是擦除后的Object方法則會執行父類的方法,打印出“Node.setData”
運行結果: MyNode.setData:1213
可見執行的卻是子類的方法?完成了多態的實現。這是怎么解決的?
——橋方法:顧名思義,因為擦除之后子類中方法的參數列表與父類參數列表不同,不能形成重寫,所以編譯器在編譯的時候,擦除后往子類中插入一些方法用來重載父類中的所有泛型擦除之后的Object方法,並在方法內部調用相對應的子類方法,以此重新形成父子之間多態,這些方法被稱為橋方法。(下面是編譯器擦除編譯之后的內容)
class Node { public void setData(Object data) { System.out.println("Node.setData"); } } class MyNode extends Node { // 編譯器生成的橋方法 public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData:"+data); } }