泛型是Java 1.5引入的新特性桩砰。泛型的本質(zhì)是參數(shù)化類型拓春,這種參數(shù)類型可以用在類、變量亚隅、接口和方法的創(chuàng)建中硼莽,分別稱為泛型類、泛型變量枢步、泛型接口、泛型方法渐尿。將集合聲明參數(shù)化以及使用JDK提供的泛型和泛型方法是相對簡單的醉途,而編寫自己的泛型類型會比較困難,但是還是值得思考與學習如何去編寫砖茸。
1隘擎、泛型的優(yōu)勢
提高代碼的安全性和表述性
在沒有泛型的情況的下,通過對類型Object
的引用來實現(xiàn)參數(shù)的“任意化”凉夯,缺點是要做顯式的強制類型轉(zhuǎn)換货葬,而這種轉(zhuǎn)換是要求開發(fā)者對實際參數(shù)類型可以預(yù)知的情況下進行的。一個錯誤的示范如下:
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add("String");
int isInt = (int) list.get(1); //ClassCastException
}
本例中對于強制類型轉(zhuǎn)換錯誤的情況劲够,編譯器在編譯時并不提示錯誤震桶,在運行的時候才出現(xiàn)ClassCastException
異常,這樣便存在著安全隱患征绎。(Effective Java第23條:請不要在新代碼中使用原生態(tài)類型)
提高代碼的重用率
利用泛型類可以選擇具體的類型對類進行復(fù)用相對比較容易理解蹲姐,具體的說明如下:
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
這樣我們的Box類便可以得到復(fù)用,我們可以將T替換成任何我們想要的類型:
Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();
2人柿、泛型的使用
泛型類
泛型類中使用通配泛型T相比較用Object
類型強制轉(zhuǎn)換的優(yōu)勢已經(jīng)介紹過柴墩,詳見章節(jié)1中Box
類中泛型的使用。
泛型方法
泛型類在多個方法簽名間實施類型約束凫岖。在 List<V>
中江咳,類型參數(shù) V
出現(xiàn)在 get()
、add()
哥放、contains()
等方法的簽名中歼指。當創(chuàng)建一個 Map<K, V>
類型的變量時爹土,您就在方法之間宣稱一個類型約束。您傳遞給 add()
的值將與 get()
返回的值的類型相同东臀。
類似地着饥,之所以聲明泛型方法,一般是因為您想要在該方法的多個參數(shù)之間宣稱一個類型約束惰赋。舉例如下:
public static void main(String[] args) throws ClassNotFoundException {
String str=get("Hello", "World");
System.out.println(str);
}
public static <T, U> T get(T t, U u) {
if (u != null)
return t;
else
return null;
}
泛型變量
在泛型類宰掉、泛型方法的介紹中,我們已經(jīng)使用到了泛型變量赁濒,申明泛型變量主要是因為我們在定義泛型變量的時候轨奄,我們并不知道這個泛型類型T,到底是什么類型拒炎,所以挪拟,只能默認T為原始類型Object,而是使用時確定泛型T的具體類型击你,也是用來做類型限定的玉组。
通配符
通配泛型的使用相對基本的泛型類型的使用而言具有一定的難度,不過通配符可以提高API的靈活性丁侄。舉例如下定義3個類:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
通過通配泛型惯雳,可以定義出受檢的泛型類型,也能夠?qū)讉€類的關(guān)系體現(xiàn)出來鸿摇。
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
3石景、數(shù)組與泛型
數(shù)組與泛型相比,有兩個重要的不同點拙吉。首先潮孽,數(shù)組是協(xié)變的(covariant
)。這就是說如果sub
是super
的子類型筷黔,那么數(shù)組類型sub[]
就是super[]
的子類型往史。然而,泛型是不可變的(invariant
)佛舱,對于任意兩個不同的類型type1
和type2
怠堪,List<type1>
既不是List<type1>
的子類型,也不是List<type2>
的超類型名眉。
數(shù)組和泛型的第二大區(qū)別在于數(shù)組是具體化的粟矿,因此數(shù)組會在運行時才知道并檢查他們的元素類型約束。相比之下损拢,泛型是通過擦除來實現(xiàn)的陌粹,因此泛型只在編譯時強化他們的類型信息,并在運行時丟棄他們的元素類型信息福压。
Object[] arr = new Long[1];
arr[0] = "I don't fit in"; //運行失敗掏秩,拋出ArrayStoreException
List<Object> list = new ArrayList<>(); //編譯不通過或舞,類型不匹配
list.add(I don't fit in);
(Effective Java第25條:列表優(yōu)先于數(shù)組)
由于以上這些根本的區(qū)別,數(shù)組和泛型不能很好的混合使用蒙幻,例如:創(chuàng)建泛型或者類型參數(shù)的數(shù)組是非法的映凳。
4、類型擦除
不同的語言在實現(xiàn)泛型時采用的方式不同邮破,C++的模板會在編譯時根據(jù)參數(shù)類型的不同生成不同的代碼诈豌,而Java的泛型是一種偽泛型,編譯為字節(jié)碼時參數(shù)類型會在代碼中被擦除抒和,單獨記錄在Class文件的attributes
域矫渔,而在使用泛型處做類型檢查與類型轉(zhuǎn)換。
TIPS: 區(qū)別Java語言的編譯時和運行時是非常重要的摧莽,泛型只在編譯時強化他們的類型信息庙洼,并在運行時丟棄他們的元素類型信息。泛型的運行時擦除可以通過Java提供的反射機制進行證明镊辕,比如通過反射調(diào)用List<String>
容器的add()
方法油够,繞過泛型檢查,成功插入Integer
類型的變量征懈。
假設(shè)參數(shù)類型的占位符為T石咬,擦除規(guī)則如下:
-
<T>
擦除后變?yōu)?code>Obecjt -
<? extends A>
擦除后變?yōu)?code>A
*<? super A>
擦除后變?yōu)?code>Object
上述擦除規(guī)則叫做保留上界受裹。泛型擦除之后保留原始類型碌补。原始類型raw type
就是擦除去了泛型信息虏束,最后在字節(jié)碼中的類型變量的真正類型棉饶。無論何時定義一個泛型類型,相應(yīng)的原始類型都會被自動地提供镇匀。類型變量被擦除crased
照藻,并使用其限定類型(無限定的變量用Object
)替換。
但是要區(qū)分原始類型和泛型變量的類型
在調(diào)用泛型方法的時候汗侵,可以指定泛型幸缕,也可以不指定泛型。
在不指定泛型的情況下晰韵,泛型變量的類型為 該方法中的幾種類型的同一個父類的最小級发乔,直到Object。
在指定泛型的時候雪猪,該方法中的幾種類型必須是該泛型實例類型或者其子類栏尚。
public class Test2{
public static void main(String[] args) {
/**不指定泛型的時候*/
int i=Test2.add(1, 2); //兩參數(shù)都是Integer,所以T為Integer類型
Number f=Test2.add(1 , 1.2);//參數(shù)是Integer和Float只恨,取同一父類的最小級Number
Object o=Test2.add(1, "asd"); //參數(shù)是Integer和String译仗,取同一父類的最小級Object
/**指定泛型的時候*/
int a=Test2.<Integer>add(1, 2);//指定了Integer抬虽,所以只能為Integer類型或者其子類
int b=Test2.<Integer>add(1 , 2.2);//編譯錯誤,指定了Integer纵菌,不能為Float
Number c=Test2.<Number>add(1, 2.2); //指定為Number阐污,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
5、類型擦除的問題和解決方法
Java的泛型是偽泛型咱圆。為什么說Java的泛型是偽泛型呢笛辟?因為,在編譯期間闷堡,所有的泛型信息都會被擦除掉隘膘。正確理解泛型概念的首要前提是理解類型擦出(type erasure
)。Java中的泛型基本上都是在編譯器這個層次來實現(xiàn)的杠览。在生成的Java字節(jié)碼中是不包含泛型中的類型信息的弯菊。使用泛型的時候加上的類型參數(shù),會在編譯器在編譯的時候去掉踱阿。這個過程就稱為類型擦除管钳。
因為種種原因,Java不能實現(xiàn)真正的泛型软舌,只能使用類型擦除來實現(xiàn)偽泛型才漆,這樣雖然不會有類型膨脹的問題,但是也引起了許多新的問題佛点。所以醇滥,Sun對這些問題作出了許多限制,避免我們犯各種錯誤超营。
1鸳玩、先檢查,在編譯演闭,以及檢查編譯的對象和引用傳遞的問題
2不跟、自動類型轉(zhuǎn)換
因為類型擦除的問題,所以所有的泛型類型變量最后都會被替換為原始類型米碰。這樣就引起了一個問題窝革,既然都被替換為原始類型,那么為什么我們在獲取的時候吕座,不需要進行強制類型轉(zhuǎn)換呢虐译?實際上,使用泛型的容器會在return
之前吴趴,會根據(jù)泛型變量進行強轉(zhuǎn)漆诽。
3、類型擦除與多態(tài)的沖突和解決方法
子類實現(xiàn)父類中的泛型的方法時注意因為擦除而引起的語義的變化
4、泛型類型變量不能是基本數(shù)據(jù)類型
不能用類型參數(shù)替換基本類型拴泌。就比如魏身,沒有ArrayList<double>
,只有ArrayList<Double>
蚪腐。因為當類型擦除后箭昵,ArrayList
的原始類型變?yōu)?code>Object,但是Object
類型不能存儲double
值回季,只能引用Double
的值家制。
5、運行時類型查詢
由于運行時類型已經(jīng)擦除泡一,所以進行泛型類型的查詢是不正確的颤殴,對泛型的類型查詢Java限定了這種類型查詢的方式if( arrayList instanceof ArrayList<?>)
6、異常中使用泛型的問題
不能拋出也不能捕獲泛型類的對象鼻忠。因為異常都是在運行時捕獲和拋出的涵但,而在編譯的時候,泛型信息全都會被擦除掉帖蔓,類型信息被擦除后矮瘟,那么很有可能兩個地方的catch
都變?yōu)樵碱愋?code>Object,這個當然就是不行的塑娇。就好比澈侠,catch
兩個一模一樣的普通異常,不能通過編譯一樣埋酬。
根據(jù)異常捕獲的原則哨啃,一定是子類在前面,父類在后面写妥,那么上面就違背了這個原則拳球。即使你在使用該靜態(tài)方法的使用T是ArrayIndexOutofBounds
,在編譯之后還是會變成Throwable
耳标,ArrayIndexOutofBounds
是IndexOutofBounds
的子類醇坝,違背了異常捕獲的原則邑跪。所以Java為了避免這樣的情況次坡,禁止在catch
子句中使用泛型變量。
7画畅、泛型類型的實例化
不能實例化泛型類型
8砸琅、類型擦除后的沖突
當泛型類型被擦除后,創(chuàng)建條件不能產(chǎn)生沖突轴踱,如下代碼段中泛型擦除后方法
boolean equals(T)
變成了方法boolean equals(Object)
這與Object.equals
方法是沖突的症脂!當然,補救的辦法是重新命名引發(fā)錯誤的方法。
class Pair<T> {
public boolean equals(T value) {
return null;
}
}
9诱篷、泛型在靜態(tài)方法和靜態(tài)類中的問題
泛型類中的靜態(tài)方法和靜態(tài)變量不可以使用泛型類所聲明的泛型類型參數(shù)壶唤。因為泛型類中的泛型參數(shù)的實例化是在定義對象的時候指定的,而靜態(tài)變量和靜態(tài)方法不需要使用對象來調(diào)用棕所。對象都沒有創(chuàng)建闸盔,如何確定這個泛型參數(shù)是何種類型,所以當然是錯誤的琳省。但是要注意區(qū)分一種情況迎吵,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T针贬,是沒有錯誤的击费。
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
public static <T>T show(T one){//這是正確的
return null;
}
}
參考資料:
[1]:《Effective Java》
[2]:關(guān)于Java泛型深入理解小總結(jié)
[3]:Java泛型詳解
[4]:Java泛型的實現(xiàn):原理與問題
[5]:Java中的逆變與協(xié)變
[6]:java泛型(一)、泛型的基本介紹和使用
[7]:java泛型(二)桦他、泛型的內(nèi)部原理:類型擦除以及類型擦除帶來的問題