新聞中心
關(guān)于Java泛型的教程,免費(fèi)的,不免費(fèi)的,有很多。我遇到的最好的教材有:

我們一直強(qiáng)調(diào)網(wǎng)站設(shè)計(jì)制作、做網(wǎng)站對(duì)于企業(yè)的重要性,如果您也覺(jué)得重要,那么就需要我們慎重對(duì)待,選擇一個(gè)安全靠譜的網(wǎng)站建設(shè)公司,企業(yè)網(wǎng)站我們建議是要么不做,要么就做好,讓網(wǎng)站能真正成為企業(yè)發(fā)展過(guò)程中的有力推手。專業(yè)網(wǎng)絡(luò)公司不一定是大公司,成都創(chuàng)新互聯(lián)公司作為專業(yè)的網(wǎng)絡(luò)公司選擇我們就是放心。
The Java Tutorial
Java Generics and Collections, by Maurice Naftalin and Philip Wadler
Effective Java中文版(第2版), by Joshua Bloch.
盡管有這么多豐富的資料,有時(shí)我感覺(jué),有很多的程序員仍然不太明白Java泛型的功用和意義。這就是為什么我想使用一種最簡(jiǎn)單的形式來(lái)總結(jié)一下程序員需要知道的關(guān)于Java泛型的最基本的知識(shí)。
Java泛型由來(lái)的動(dòng)機(jī)
理解Java泛型最簡(jiǎn)單的方法是把它看成一種便捷語(yǔ)法,能節(jié)省你某些Java類型轉(zhuǎn)換(casting)上的操作:
- List
box = ...; - Apple apple = box.get(0);
上面的代碼自身已表達(dá)的很清楚:box是一個(gè)裝有Apple對(duì)象的List。get方法返回一個(gè)Apple對(duì)象實(shí)例,這個(gè)過(guò)程不需要進(jìn)行類型轉(zhuǎn)換。沒(méi)有泛型,上面的代碼需要寫(xiě)成這樣:
- List box = ...;
- Apple apple = (Apple) box.get(0);
很明顯,泛型的主要好處就是讓編譯器保留參數(shù)的類型信息,執(zhí)行類型檢查,執(zhí)行類型轉(zhuǎn)換操作:編譯器保證了這些類型轉(zhuǎn)換的絕對(duì)無(wú)誤。
相對(duì)于依賴程序員來(lái)記住對(duì)象類型、執(zhí)行類型轉(zhuǎn)換——這會(huì)導(dǎo)致程序運(yùn)行時(shí)的失敗,很難調(diào)試和解決,而編譯器能夠幫助程序員在編譯時(shí)強(qiáng)制進(jìn)行大量的類型檢查,發(fā)現(xiàn)其中的錯(cuò)誤。
泛型的構(gòu)成
由泛型的構(gòu)成引出了一個(gè)類型變量的概念。根據(jù)Java語(yǔ)言規(guī)范,類型變量是一種沒(méi)有限制的標(biāo)志符,產(chǎn)生于以下幾種情況:
◆ 泛型類聲明
◆ 泛型接口聲明
◆ 泛型方法聲明
◆ 泛型構(gòu)造器(constructor)聲明
泛型類和接口
如果一個(gè)類或接口上有一個(gè)或多個(gè)類型變量,那它就是泛型。類型變量由尖括號(hào)界定,放在類或接口名的后面:
- public interface List
extends Collection { - ...
- }
簡(jiǎn)單的說(shuō),類型變量扮演的角色就如同一個(gè)參數(shù),它提供給編譯器用來(lái)類型檢查的信息。
Java類庫(kù)里的很多類,例如整個(gè)Collection框架都做了泛型化的修改。例如,我們?cè)谏厦娴牡谝欢未a里用到的List接口就是一個(gè)泛型類。在那段代碼里,box是一個(gè)List 對(duì)象,它是一個(gè)帶有一個(gè)Apple類型變量的List接口的類實(shí)現(xiàn)的實(shí)例。編譯器使用這個(gè)類型變量參數(shù)在get方法被調(diào)用、返回一個(gè)Apple對(duì)象時(shí)自動(dòng)對(duì)其進(jìn)行類型轉(zhuǎn)換。
實(shí)際上,這新出現(xiàn)的泛型標(biāo)記,或者說(shuō)這個(gè)List接口里的get方法是這樣的:
- T get(int index);
get方法實(shí)際返回的是一個(gè)類型為T(mén)的對(duì)象,T是在List
泛型方法和構(gòu)造器(Constructor)
非常的相似,如果方法和構(gòu)造器上聲明了一個(gè)或多個(gè)類型變量,它們也可以泛型化。
- public static
T getFirst(List list)
這個(gè)方法將會(huì)接受一個(gè)List
例子
你既可以使用Java類庫(kù)里提供的泛型類,也可以使用自己的泛型類。
類型安全的寫(xiě)入數(shù)據(jù)…
下面的這段代碼是個(gè)例子,我們創(chuàng)建了一個(gè)List
- List
str = new ArrayList (); - str.add("Hello ");
- str.add("World.");
如果我們?cè)噲D在List
- str.add(1); // 不能編譯
類型安全的讀取數(shù)據(jù)…
當(dāng)我們?cè)谑褂肔ist
- String myString = str.get(0);
遍歷
類庫(kù)中的很多類,諸如Iterator
- for (Iterator
iter = str.iterator(); iter.hasNext();) { - String s = iter.next();
- System.out.print(s);
- }
使用foreach
“for each”語(yǔ)法同樣受益于泛型。前面的代碼可以寫(xiě)出這樣:
- for (String s: str) {
- System.out.print(s);
- }
這樣既容易閱讀也容易維護(hù)。
自動(dòng)封裝(Autoboxing)和自動(dòng)拆封(Autounboxing)
在使用Java泛型時(shí),autoboxing/autounboxing這兩個(gè)特征會(huì)被自動(dòng)的用到,就像下面的這段代碼:
- List
ints = new ArrayList (); - ints.add(0);
- ints.add(1);
- int sum = 0;
- for (int i : ints) {
- sum += i;
- }
然而,你要明白的一點(diǎn)是,封裝和解封會(huì)帶來(lái)性能上的損失,所有,通用要謹(jǐn)慎的使用。
子類型
在Java中,跟其它具有面向?qū)ο箢愋偷恼Z(yǔ)言一樣,類型的層級(jí)可以被設(shè)計(jì)成這樣:
在Java中,類型T的子類型既可以是類型T的一個(gè)擴(kuò)展,也可以是類型T的一個(gè)直接或非直接實(shí)現(xiàn)(如果T是一個(gè)接口的話)。因?yàn)椤俺蔀槟愁愋偷淖宇愋汀笔且粋€(gè)具有傳遞性質(zhì)的關(guān)系,如果類型A是B的一個(gè)子類型,B是C的子類型,那么A也是C的子類型。在上面的圖中:
◆ FujiApple(富士蘋(píng)果)是Apple的子類型
◆ Apple是Fruit(水果)的子類型
◆ FujiApple(富士蘋(píng)果)是Fruit(水果)的子類型
所有Java類型都是Object類型的子類型。
B類型的任何一個(gè)子類型A都可以被賦給一個(gè)類型B的聲明:
- Apple a = ...;
- Fruit f = a;
泛型類型的子類型
如果一個(gè)Apple對(duì)象的實(shí)例可以被賦給一個(gè)Fruit對(duì)象的聲明,就像上面看到的,那么,List
答案會(huì)出乎你的意料:沒(méi)有任何關(guān)系。用更通俗的話,泛型類型跟其是否子類型沒(méi)有任何關(guān)系。
這意味著下面的這段代碼是無(wú)效的:
- List
apples = ...; - List
fruits = apples;
下面的同樣也不允許:
- List
apples; - List
fruits = ...; - apples = fruits;
為什么?一個(gè)蘋(píng)果是一個(gè)水果,為什么一箱蘋(píng)果不能是一箱水果?
在某些事情上,這種說(shuō)法可以成立,但在類型(類)封裝的狀態(tài)和操作上不成立。如果把一箱蘋(píng)果當(dāng)成一箱水果會(huì)發(fā)生什么情況?
- List
apples = ...; - List
fruits = apples; - fruits.add(new Strawberry());
如果可以這樣的話,我們就可以在list里裝入各種不同的水果子類型,這是絕對(duì)不允許的。
另外一種方式會(huì)讓你有更直觀的理解:一箱水果不是一箱蘋(píng)果,因?yàn)樗锌赡苁且幌淞硗庖环N水果,比如草莓(子類型)。
#p#
這是一個(gè)需要注意的問(wèn)題嗎?
應(yīng)該不是個(gè)大問(wèn)題。而程序員對(duì)此感到意外的最大原因是數(shù)組和泛型類型上用法的不一致。對(duì)于泛型類型,它們和類型的子類型之間是沒(méi)什么關(guān)系的。而對(duì)于數(shù)組,它們和子類型是相關(guān)的:如果類型A是類型B的子類型,那么A[]是B[]的子類型:
- Apple[] apples = ...;
- Fruit[] fruits = apples;
可是稍等一下!如果我們把前面的那個(gè)議論中暴露出的問(wèn)題放在這里,我們?nèi)匀荒軌蛟谝粋€(gè)apple類型的數(shù)組中加入strawberrie(草莓)對(duì)象:
- Apple[] apples = new Apple[1];
- Fruit[] fruits = apples;
- fruits[0] = new Strawberry();
這樣寫(xiě)真的可以編譯,但是在運(yùn)行時(shí)拋出ArrayStoreException異常。因?yàn)閿?shù)組的這特點(diǎn),在存儲(chǔ)數(shù)據(jù)的操作上,Java運(yùn)行時(shí)需要檢查類型的兼容性。這種檢查,很顯然,會(huì)帶來(lái)一定的性能問(wèn)題,你需要明白這一點(diǎn)。
重申一下,泛型使用起來(lái)更安全,能“糾正”Java數(shù)組中這種類型上的缺陷。
現(xiàn)在估計(jì)你會(huì)感到很奇怪,為什么在數(shù)組上會(huì)有這種類型和子類型的關(guān)系,我來(lái)給你一個(gè)《Java Generics and Collections》這本書(shū)上給出的答案:如果它們不相關(guān),你就沒(méi)有辦法把一個(gè)未知類型的對(duì)象數(shù)組傳入一個(gè)方法里(不經(jīng)過(guò)每次都封裝成Object[]),就像下面的:
- void sort(Object[] o);
泛型出現(xiàn)后,數(shù)組的這個(gè)個(gè)性已經(jīng)不再有使用上的必要了(下面一部分我們會(huì)談到這個(gè)),實(shí)際上是應(yīng)該避免使用。
通配符
在本文的前面的部分里已經(jīng)說(shuō)過(guò)了泛型類型的子類型的不相關(guān)性。但有些時(shí)候,我們希望能夠像使用普通類型那樣使用泛型類型:
◆ 向上造型一個(gè)泛型對(duì)象的引用
◆ 向下造型一個(gè)泛型對(duì)象的引用
向上造型一個(gè)泛型對(duì)象的引用
例如,假設(shè)我們有很多箱子,每個(gè)箱子里都裝有不同的水果,我們需要找到一種方法能夠通用的處理任何一箱水果。更通俗的說(shuō)法,A是B的子類型,我們需要找到一種方法能夠?qū)類型的實(shí)例賦給一個(gè)C類型的聲明。
為了完成這種操作,我們需要使用帶有通配符的擴(kuò)展聲明,就像下面的例子里那樣:
- List
apples = new ArrayList (); - List extends Fruit> fruits = apples;
“? extends”是泛型類型的子類型相關(guān)性成為現(xiàn)實(shí):Apple是Fruit的子類型,List
向下造型一個(gè)泛型對(duì)象的引用
現(xiàn)在我來(lái)介紹另外一種通配符:? super。如果類型B是類型A的超類型(父類型),那么C 是 C super A> 的子類型:
- List
fruits = new ArrayList (); - List super Apple> = fruits;
為什么使用通配符標(biāo)記能行得通?
原理現(xiàn)在已經(jīng)很明白:我們?nèi)绾卫眠@種新的語(yǔ)法結(jié)構(gòu)?
? extends
讓我們重新看看這第二部分使用的一個(gè)例子,其中談到了Java數(shù)組的子類型相關(guān)性:
- Apple[] apples = new Apple[1];
- Fruit[] fruits = apples;
- fruits[0] = new Strawberry();
就像我們看到的,當(dāng)你往一個(gè)聲明為Fruit數(shù)組的Apple對(duì)象數(shù)組里加入Strawberry對(duì)象后,代碼可以編譯,但在運(yùn)行時(shí)拋出異常。
現(xiàn)在我們可以使用通配符把相關(guān)的代碼轉(zhuǎn)換成泛型:因?yàn)锳pple是Fruit的一個(gè)子類,我們使用? extends 通配符,這樣就能將一個(gè)List
- List
apples = new ArrayList (); - List extends Fruit> fruits = apples;
- fruits.add(new Strawberry());
這次,代碼就編譯不過(guò)去了!Java編譯器會(huì)阻止你往一個(gè)Fruit list里加入strawberry。在編譯時(shí)我們就能檢測(cè)到錯(cuò)誤,在運(yùn)行時(shí)就不需要進(jìn)行檢查來(lái)確保往列表里加入不兼容的類型了。即使你往list里加入Fruit對(duì)象也不行:
- fruits.add(new Fruit());
你沒(méi)有辦法做到這些。事實(shí)上你不能夠往一個(gè)使用了? extends的數(shù)據(jù)結(jié)構(gòu)里寫(xiě)入任何的值。
原因非常的簡(jiǎn)單,你可以這樣想:這個(gè)? extends T 通配符告訴編譯器我們?cè)谔幚硪粋€(gè)類型T的子類型,但我們不知道這個(gè)子類型究竟是什么。因?yàn)闆](méi)法確定,為了保證類型安全,我們就不允許往里面加入任何這種類型的數(shù)據(jù)。另一方面,因?yàn)槲覀冎?,不論它是什么類型,它總是類型T的子類型,當(dāng)我們?cè)谧x取數(shù)據(jù)時(shí),能確保得到的數(shù)據(jù)是一個(gè)T類型的實(shí)例:
- Fruit get = fruits.get(0);
? super
使用 ? super 通配符一般是什么情況?讓我們先看看這個(gè):
- List
fruits = new ArrayList (); - List super Apple> = fruits;
我們看到fruits指向的是一個(gè)裝有Apple的某種超類(supertype)的List。同樣的,我們不知道究竟是什么超類,但我們知道Apple和任何Apple的子類都跟它的類型兼容。既然這個(gè)未知的類型即是Apple,也是GreenApple的超類,我們就可以寫(xiě)入:
- fruits.add(new Apple());
- fruits.add(new GreenApple());
如果我們想往里面加入Apple的超類,編譯器就會(huì)警告你:
- fruits.add(new Fruit());
- fruits.add(new Object());
因?yàn)槲覀儾恢浪窃鯓拥某?,所有這樣的實(shí)例就不允許加入。
從這種形式的類型里獲取數(shù)據(jù)又是怎么樣的呢?結(jié)果表明,你只能取出Object實(shí)例:因?yàn)槲覀儾恢莱惥烤故鞘裁?,編譯器唯一能保證的只是它是個(gè)Object,因?yàn)镺bject是任何Java類型的超類。
存取原則和PECS法則
總結(jié) ? extends 和 the ? super 通配符的特征,我們可以得出以下結(jié)論:
◆ 如果你想從一個(gè)數(shù)據(jù)類型里獲取數(shù)據(jù),使用 ? extends 通配符
◆ 如果你想把對(duì)象寫(xiě)入一個(gè)數(shù)據(jù)結(jié)構(gòu)里,使用 ? super 通配符
◆ 如果你既想存,又想取,那就別用通配符。
這就是Maurice Naftalin在他的《Java Generics and Collections》這本書(shū)中所說(shuō)的存取原則,以及Joshua Bloch在他的《Effective Java》這本書(shū)中所說(shuō)的PECS法則。
Bloch提醒說(shuō),這PECS是指”P(pán)roducer Extends, Consumer Super”,這個(gè)更容易記憶和運(yùn)用。
【編輯推薦】
- 向Java開(kāi)戰(zhàn)?別搞錯(cuò)了對(duì)象
- 關(guān)于Java對(duì)象序列化您不知道的5件事
- Java侵權(quán)案鮮為人知的歷史內(nèi)幕:谷歌"螳螂捕蟬"
- 菜鳥(niǎo)入門(mén) java語(yǔ)言學(xué)習(xí)六大要點(diǎn)
- 相同中的不同:Java程序員應(yīng)該停止低看C#
分享文章:Java泛型簡(jiǎn)明教程
瀏覽地址:http://m.fisionsoft.com.cn/article/coosiph.html


咨詢
建站咨詢
