泛型怎麼玩
1.什麼是泛型
參數化類型,把類型當做參數傳遞給類或者方法,創建對象或者調用方法才明確類型;
Java泛型的設計原則:只要在編譯期沒有錯誤或警告,在運行期就不回出現ClassCastException異常。
2.泛型的好處
如集合類,如果沒有泛型之前,我們不清楚集合類的元素類型,很有可能會發生ClassCastException異常或者需要類型裝置轉換,如:
List list = new ArrayList();
list.add("123");
list.add(new Integer(123));
Object obj = list.get(0);
List對內部元素是沒有任何限制的,裝載String或者Integer都是沒有任何語法錯誤的,只知道是Object類型,當get的時候只能返回Object,如要返回String則需要進行類型向下強制轉換,這樣程序就不安全,使用不當就會發生異常。
支持過泛型類之後,可以把類型傳給List(指定List內部元素類型),在編譯期就確定了List的元素類型,使得程序可以更安全、健壯,如:
List<String> list = new ArrayList<String>();
list.add("123");
list.add(new Integer(123));//編譯期會發出錯誤警告
String str = list.get(0);
有了泛型後,get獲取元素可以直接返回String,避免類型的強制轉換,提高了:
- 代碼可讀性 (可以更清晰的讀出包裝類型限制)
- 代碼的健壯性[避免了集合類型的強制轉換]
- 代碼更簡潔 (可以更優雅的實現需要動態設定參數類型功能等)
3.如何使用泛型
但出於規範的目的,Java 還是建議我們用單個大寫字母來代表類型參數。常見的如:
泛型類
泛型類就是把泛型定義在類上,在使用此類的時候需要關聯泛型參數時才明確下來,這樣的話可以動態的使用一類抽象類(需要使用才明確什麼類型),不需要擔心轉換問題。
定義在類上,該類的所有方法、屬性都可以使用這一類型參數:
public class Test<T> {
private T obj;
public Test(T Obj){
this.obj = obj;
}
public static void main(String[] args) {
Test<String> t = new Test<String>("123");
String a = t.obj;
Test<Integer> t1 = new Test<Integer>(123);
Integer b = t1.obj;
}
}
用戶想要使用哪種類型,就在創建的時候指定類型。使用的時候,該類就會自動轉換成用戶想要使用的類型了。
**上例代碼的T泛型參數,是JAVA的常用寫法,你也可以換成任意的字符;但出於規範的目的,Java還是建議我們用單個大寫字母來代表類型參數。**常見的如:
- T 代表一般的任何類。
- E 代表 Element 的意思,或者 Exception 異常的意思。
- K 代表 Key 的意思。
- V 代表 Value 的意思,通常與 K 一起配合使用。
- S 代表 Subtype 的意思。
泛型類派生出的子類
上述泛型類本質上還是一個JAVA類,只不過多了泛型類型這個參數,所以泛型類也是可以繼承的。
但子類可以明確泛型類型參數變量,也可以不明確:
子類明確泛型類型參數
public class Test<T> {
private T obj;
public Test(T Obj){
this.obj = obj;
}
}
/**
* 子類明確泛型參數類型
*/
class TestA extends Test<String>{
public TestA(String Obj) {
super(Obj);
}
}
子類不明確泛型類型參數
public class Test<T> {
private T obj;
public Test(T Obj){
this.obj = obj;
}
}
/**
* 子類不明確泛型參數類型
*/
class TestA<T> extends Test<T>{
public TestA(T Obj) {
super(Obj);
}
}
當子類不明確泛型類的類型參數變量時,外界使用子類的時候,也需要傳遞類型參數變量進來,在實現類上需要定義出類型參數變量
泛型方法
在某一個方法上使用泛型,有時外界只關心某個方法,而不關心整個類,就可以使用泛型方法
定義泛型方法:
public class Test {
public <T> void print(T t){
System.out.println(t);
}
public static void main(String[] args) {
Test t = new Test();
//用方法,傳入的參數是什麼類型,返回值就是什麼類型
t.print("123");
t.print(123);
}
}
泛型通配符
類型通配符一般是使用?代替具體的類型參數,例如 List<?> 在邏輯上是List,List 等所有List<具體類型實參>的父類。
public class Test{
public void test(List<?> list){
//只讀,不能使用add一類的操作
list.get(1);
}
public static void main(String[] args) {
Test t = new Test();
//用方法,傳入的參數是什麼類型,返回值就是什麼類型
t.test(new ArrayList<String>());
t.test(new ArrayList<Integer>());
}
}
注意:?泛型對象是隻讀的,不可修改,因爲?類型是不確定的,可以代表範圍內任意類型;
類型通配符上限
可以設定?類型通配符可接受類型的上限,只能裝載上限的子類或自身
? extends Number
類型通配符下限
顧名思義,可以設定?類型通配符可接受類型的下限,只能裝載下限的父類或自身
//傳遞進來的只能是Type或Type的父類
? super Number
通配符和泛型方法
所有能用類型通配符(?)解決的問題都能用泛型方法解決,並且泛型方法可以解決的更好:
a. 類型通配符:void func(List<? extends A> list);
b. 完全可以用泛型方法解決:<T extends A> void func(List<T> list);
上面兩種方法可以達到相同的效果(?可以代表範圍內任意類型,而T也可以傳入範圍內的任意類型實參),並且泛型方法更進一步,?泛型對象是隻讀的,而泛型方法裏的泛型對象是可修改的,即List list中的list是可修改的!
兩者區別:
通配符可以使用超類(super)限定而類型參數不行
?和 T 都表示不確定的類型,區別在於我們可以對 T 進行操作,但是對 ?不行
類型參數可以多重限定而通配符不行(T extends A,B)
?相對來說更靈活,代碼更簡潔,二者各有各的應用場景:
- 一般只讀就用?,要修改就用泛型方法
- 在多個參數、返回值之間存在類型依賴關係就應該使用泛型方法,否則就應該是通配符?
4.類型擦除
Java的類庫是Java生態中非常寶貴的財富,必須保證向後兼容(即現有的代碼和類文件依舊合法)和遷移兼容(泛化的代碼和非泛化的代碼可互相調用)基於上面這兩個背景和考慮,Java設計者採取了“類型擦除”這種折中的實現方式。
Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成爲類型擦除。
原始類型
類型擦除之後爲什麼,我們需要進行類型轉換呢?原因是類型擦除之後,最後在字節碼中的類型變量會轉譯成原始類型。
在無限定泛型類型,會被轉譯成Object的原始類型。
有限定泛型類型,會被其限定類型替換
帶來的問題
多態的衝突
子類重寫父類方法,如父類方法使用了泛型,由於類型擦除,父類泛型參數都會轉譯成Object類型,子類重寫方法明確泛型類型,就會導致參數類型不一致,重載@Override不成立。
如何處理呢?
橋方法:在子類變異後,會生成橋方法來實現重👟父親
反射調用問題
既然泛型類型在編譯期都會被擦除,轉移成上限(Object),那通過反射的方式調用機會導致,類型不匹配胃問題有可能導致ClassCastException異常:
public class Test{
public <T> void test(List<T> list,T e){
//只讀,不能使用add一類的操作
list.add(e);
}
public <T> void print(List<T> list){
if(list!=null){
for(T t: list){
System.out.println(t);
}
}
}
public static void main(String[] args) throws Exception{
List<String> list = new ArrayList<String>();
Test t = new Test();
t.test(list,"123");
//一定寫object.class 表示什麼類型都可以加
Method method = Test.class.getMethod("test", List.class,Object.class);
method.invoke(t, list,new Integer(456));
t.print(list);
}
}
上例,list限定泛型類型爲String,但是class字節碼把String參數類型擦除,通過反射調用,可以傳入Object對象,從而把Integer對象加入List。