2.6 Java泛型詳解
Java泛型是JDK5中引入的一個(gè)新特性,允許在定義類和接口的時(shí)候使用類型參數(shù)(type parameter),聲明的類型參數(shù)在使用時(shí)用具體的類型來替換蝴乔。泛型最主要的應(yīng)用是在JDK5中的新集合類框架中。
2.6.1 類型擦除
首先我們看一下Java泛型中的類型擦除:在生成的Java字節(jié)碼中是不包含泛型的類型信息的癞尚,使用泛型的時(shí)候加上的類型參數(shù)在編譯的時(shí)候會(huì)被編譯器去掉毡惜,這個(gè)過程就稱為類型擦除渊胸。如在代碼中定義的List<Object>和List<String>等類型橱健,在編譯之后都會(huì)變成List而钞。JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM來說是不可見的(反射是可見的拘荡,具體可參見2.3中筆記)臼节,這一點(diǎn)和C++模板機(jī)制實(shí)現(xiàn)泛型有很大區(qū)別。
很多泛型的奇怪特性都與類型擦除有關(guān),比如:
- 泛型類并沒有自己獨(dú)有的Class類對(duì)象网缝。比如并不存在List<String>.class或者List<Integer>.class巨税,而只有List.class。
- 靜態(tài)變量是被泛型類的所有實(shí)例所共享的途凫。對(duì)于聲明為MyClass<T>的類垢夹,訪問其中的靜態(tài)變量的方法仍然是MyClass.myStaticVar溢吻。不管是通過new MyClass<String>還是new MyClass<Integer>創(chuàng)建的對(duì)象维费,都是共享一個(gè)靜態(tài)變量。
- 泛型的類型參數(shù)不能用在Java異常處理的catch語句中促王。因?yàn)楫惓L幚硎怯蒍VM在運(yùn)行時(shí)刻來進(jìn)行的犀盟。由于類型信息被擦除,JVM無法區(qū)分兩個(gè)異常類型MyException<String>和 MyException<Integer>的蝇狼,對(duì)于JVM來說阅畴,它們都是MyException類型的,也就無法執(zhí)行與異常對(duì)應(yīng)的catch語句迅耘。
類型擦除的基本過程也比較簡單贱枣,首先是找到用來替換類型參數(shù)的具體類,一般是Object颤专,如果指定了類型參數(shù)的上界的話纽哥,則是這個(gè)上界。然后把代碼中的類型參數(shù)都替換成該類栖秕。同時(shí)去掉出現(xiàn)的類型聲明春塌,即去掉<>的內(nèi)容。比如T get()方法聲明就變成了Object get()簇捍;List<String>就變成了List只壳。接下來就可能需要生成一些橋接方法(bridge method),這是由于擦除了類型之后的類可能缺少某些必須的方法暑塑。比如考慮下面的代碼:
class MyString implements Comparable<String> {
@Override
public int compareTo(String str) {
return 0;
}
}
當(dāng)類型信息被擦除之后吼句,上述類的聲明變成了class MyString implements Comparable。但是這樣的話事格,類MyString就會(huì)有編譯錯(cuò)誤命辖,因?yàn)闆]有實(shí)現(xiàn)接口Comparable聲明的int compareTo(Object)方法,這個(gè)時(shí)候就由編譯器來動(dòng)態(tài)生成這個(gè)方法分蓖。
2.6.2 通配符與上下界
在使用泛型類的時(shí)候尔艇,既可以指定一個(gè)具體的類型,也可以用通配符?來表示未知類型么鹤,如List<?>就聲明了List中包含的元素類型是未知的终娃。通配符所代表的其實(shí)是一組類型,但具體的類型是未知的蒸甜。通配符分為三類:無界通配符棠耕、上界通配符和下界通配符余佛。通配符本身比較復(fù)雜,我們會(huì)以集合為代表窍荧,以元素的添加和獲取為例簡要說明其用法辉巡。
無界通配符
“?”表示無界通配符,List<?>表示:List中存儲(chǔ)的元素的類型是未知的蕊退。
- 添加元素郊楣,使用無界通配符時(shí),由于類型不確定的(可以是任何類型)瓤荔,不可以向List<?>添加任何元素(除了null)净蚤,因?yàn)槿绻薙tring的話,往里面添加Integer顯然是錯(cuò)誤的输硝。那為什么不能添加Object引用今瀑,是因?yàn)镺bject引用可以指向子類實(shí)例,編譯期是無法獲知其具體類型点把,所以為了類型安全橘荠,其不可以添加任何元素(除了null)。
- 獲取元素郎逃,List<?>中的元素只可以使用Object來引用哥童,因?yàn)槠湓乜隙ㄊ荗bject及其子類引用。
事實(shí)上無界通配符通常會(huì)用在以下兩種情況:
- 在業(yè)務(wù)邏輯與泛型類型無關(guān)衣厘,如List.size和List.clean等如蚜。實(shí)際上,最常用的就是Class<?>影暴,因?yàn)镃lass<T>并沒有依賴于T错邦。
- 當(dāng)方法參數(shù)是原始的Object類型,如下:
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + "");
}
//使用泛型類替換
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + "");
}
這樣就可以兼容更多的輸出型宙,而不單純是List<Object>撬呢,如下:
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
上界通配符
“? extends Animal”表示通配符的上界是Animal,即“? extends Animal”可以代表Animal及其子類妆兑,不能代表Animal父類魂拦。
首先要闡明一點(diǎn),上界通配符和下界通配符更多的是為了解決泛型不協(xié)變的問題搁嗓。
- 添加元素芯勘,使用上界通配符時(shí),其類型仍然是不確定的(會(huì)是某個(gè)類型及其子類型)腺逛,所以仍然不可以向List<荷愕? extends Animal>添加任何元素(null除外),因?yàn)槿绻薈at的話,往里面添加Dog顯然是錯(cuò)誤的安疗,同樣不可以添加Object引用抛杨。
- 獲取元素,List<? extends Animal>中的元素可以用Animal來引用的荐类,因?yàn)槠湓乜隙ㄊ茿nimal及其子類引用怖现。
而且引入了上界之后,在使用類型的時(shí)候就可以使用上界類中定義的方法玉罐,因?yàn)槠渲性乜隙ㄊ茿nimal類或其子類成員引用屈嗤。
下界通配符
“? super Animal”表示通配符的下界是Animal,即“? super Animal”可以代表Animal及其父類厌小,不能代表Animal子類恢共。
- 添加元素战秋,使用下界通配符時(shí)璧亚,雖然類型仍然是不確定的(會(huì)是某個(gè)類型及其父類),但是此時(shí)可以向List<脂信? super Animal>添加Animal或者其子類型Dog元素癣蟋,因?yàn)槿绻薃nimal的話,往里面添加Dog顯然是可以的狰闪,子類型可以替換父類型疯搅。
- 獲取元素,List<? super Animal>中的元素只可以用Object來引用的埋泵,“? super Animal”可以代表Animal及其父類幔欧,所以只能通過Object來進(jìn)行應(yīng)用。
總結(jié)
PECS(Producer Extends Consumer Super)原則:頻繁往外讀取內(nèi)容的丽声,適合用上界Extends礁蔗;經(jīng)常往里插入的,適合用下界Super雁社。
通配符是實(shí)參浴井,通配符這些看起來很奇怪的特性原因在于:編譯器要保證類型安全,Object類型是所有類型的祖宗類型霉撵,而父類引用是可以引用子類對(duì)象的磺浙。
2.6.3 類型系統(tǒng)
在Java中,大家比較熟悉的是通過繼承機(jī)制而產(chǎn)生的類型體系結(jié)構(gòu)徒坡,比如String繼承自O(shè)bject撕氧。根據(jù)Liskov替換原則,子類是可以替換父類的喇完。當(dāng)需要Object類的引用的時(shí)候伦泥,如果傳入一個(gè)String對(duì)象是沒有任何問題的。但是反過來的話,即用父類的引用替換子類引用的時(shí)候奄喂,就需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換铐殃。這種自動(dòng)的子類替換父類的類型轉(zhuǎn)換機(jī)制,對(duì)于數(shù)組也是適用的(前面說過數(shù)組是協(xié)變的)跨新,String[]可以替換Object[]富腊。
而泛型的引入,對(duì)于這個(gè)類型系統(tǒng)產(chǎn)生了一定的影響域帐,正如前面提到的List<String>是不能替換掉List<Object>的赘被。引入泛型之后的類型系統(tǒng)增加了兩個(gè)維度:一個(gè)是類型參數(shù)自身的繼承體系結(jié)構(gòu),另外一個(gè)是泛型類或接口的繼承體系結(jié)構(gòu)肖揣。前者是指對(duì)于List<String>和List<Object>這樣的情況民假,類型參數(shù)String是繼承自O(shè)bject的。而后者是指List接口繼承自Collection接口龙优。對(duì)于這個(gè)類型系統(tǒng)羊异,有如下的一些規(guī)則:
- 相同類型參數(shù)的泛型類的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu),即List<String>是Collection<String>的子類型彤断,List<String>可以替換Collection<String>野舶,這種情況也適用于帶有上下界的類型聲明。
- 當(dāng)泛型類的類型聲明中使用了通配符的時(shí)候宰衙,其子類型可以在兩個(gè)維度上分別展開平道。如對(duì)Collection<? extends Number>來說,其子類型可以在Collection這個(gè)維度上展開供炼,即List<? extends Number>和Set<? extends Number>等一屋;也可以在Number這個(gè)層次上展開,即Collection<Double>和Collection<Integer>等袋哼。如此循環(huán)下去冀墨,ArrayList<Long>和HashSet<Double>等也都算是Collection<? extends Number>的子類型。
- 如果泛型類中包含多個(gè)類型參數(shù)先嬉,則對(duì)于每個(gè)類型參數(shù)分別應(yīng)用上面的規(guī)則轧苫。
2.6.4 開發(fā)自己的泛型類
泛型類與一般的Java類基本相同,只是在類和接口定義上多出來了用<>聲明的類型參數(shù)疫蔓。一個(gè)類可以有多個(gè)類型參數(shù)含懊,如MyClass<X, Y, Z>。每個(gè)類型參數(shù)在聲明的時(shí)候可以指定上界衅胀。所聲明的類型參數(shù)在Java類中可以像一般的類型一樣作為方法的參數(shù)和返回值岔乔,或是作為域和局部變量的類型。但是由于類型擦除機(jī)制滚躯,類型參
數(shù)并不能用來創(chuàng)建對(duì)象或是作為靜態(tài)變量的類型雏门『俑瑁考慮下面的泛型類中的正確和錯(cuò)誤的用法。
class ClassTest<X extends Number, Y, Z> {
private X x; //正確用法
private static Y y; //編譯錯(cuò)誤,不能用在靜態(tài)變量中
public X getFirst() { //正確用法
return x;
}
public void wrong() {
Z z = new Z(); //編譯錯(cuò)誤,不能創(chuàng)建對(duì)象
}
}
2.6.5 最佳實(shí)踐
在使用泛型的時(shí)候可以遵循一些基本的原則,從而避免一些常見的問題茁影。
- 在代碼中避免泛型類和原始類型的混用宙帝。比如List<String>和List不應(yīng)該共同使用,這樣會(huì)產(chǎn)生一些編譯器警告和潛在的運(yùn)行時(shí)異常募闲。
- 在使用帶通配符的泛型類的時(shí)候步脓,需要明確通配符所代表的一組類型的概念。由于具體的類型是未知的浩螺,很多操作是不允許的靴患。
- 泛型類最好不要同數(shù)組一塊使用。你只能創(chuàng)建new List<?>[10]這樣的數(shù)組要出,無法創(chuàng)建new List<String>[10]這樣的鸳君。這限制了數(shù)組的使用能力,而且會(huì)帶來很多費(fèi)解的問題患蹂。因此當(dāng)需要類似數(shù)組的功能時(shí)候或颊,使用集合類即可。