1、引言java
複習javac的編譯過程當中的解語法糖的時候看見了泛型擦除中的舉例,網上的資料大多比較散各針對性不一,在此作出本身的一些詳細且易懂的總結。安全
2、泛型簡介jvm
泛型是JDK 1.5的一項新特性,一種編譯器使用的範式,語法糖的一種,能保證類型安全。【注意:繼承中,子類泛型數必須很多於父類泛型數】spa
爲了方便理解,我將泛型分爲普通泛型和通配泛型code
3、泛型分類對象
一、普通泛型 blog
就是沒有設置通配的泛型,泛型表示爲某一個類。繼承
聲明時: class Test<T>{...} 接口
使用時: Test<Integer> test = new Test<Integer>(); get
做爲無界泛型,其實就是約束左右泛型必須一致。
二、通配泛型
通配泛型包括兩種,無界通配和有界通配。
【無界通配符】
<?>通配符——表示全部類型都能與它匹配
【有界通配符】
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接口
【注意:多重邊界裏,只容許第一個能爲類,後續必須爲接口】
4、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>(); // 右邊的"答案"與左邊的「答案範圍」匹配成功
5、泛型擦除
由來:一開始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方法的擦除的限制,儘可能少使用通配泛型
泛型擦除有什麼隱患,有什麼解決方法:
一、若是不加泛型繼承,擦除後會變成原始類型,因此能加入非泛型的類型。 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>(); // 此時不檢查泛型
二、擦除後泛型信息就沒了,獲取的時候再強轉?
——泛型補償:在泛型檢查的保證下,存入的都是符合泛型的對象,編譯期間利用反射獲取元素對象的類型(getClass()方法)對要傳出元素進行強轉。
三、子類繼承泛型方法,而後對其重寫並將泛型改爲真實類型,可是在擦除以後原來父類的泛型方法會變成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); } }