[java]你應(yīng)該知道的泛型(Generic)與PECS原則

本文通過一個(gè)水果籃子的例子噪漾,試圖幫助讀者理解泛型使用中的PECS原則乾颁。本文假設(shè)讀者對(duì)泛型以及泛型通配符有基礎(chǔ)性的了解梅尤。

一個(gè)水果籃子

筆者將以一個(gè)裝水果的籃子(List集合)為例,示范泛型的使用伞梯。水果的繼承關(guān)系如下:

public class Fruit {...}
public class Apple extends Fruit {...}

泛型

泛型是java1.5出現(xiàn)的語言特征写隶。在沒有泛型之前倔撞,從集合中讀取出的每個(gè)對(duì)象都必須進(jìn)行類型轉(zhuǎn)化。這樣導(dǎo)致一些類型的錯(cuò)誤只有在運(yùn)行時(shí)才能發(fā)現(xiàn):

/**不使用泛型**/
List basket = new ArrayList();//水果籃子
basket.add("水果");
Fruit fruit = (Fruit)basket.get(0);//編譯正確慕趴,運(yùn)行錯(cuò)誤

有了泛型痪蝇,就不再需要運(yùn)行時(shí)的類型轉(zhuǎn)化,可以直接告訴編譯器集合接受什么類型的對(duì)象冕房,編譯器在編譯時(shí)可以做檢查:

/**使用泛型躏啰,不再需要類型轉(zhuǎn)化**/
Fruit get = basket.get(0);

將籃子里將水果都拿出來

我寫一個(gè)方法,將水果籃子中所有水果拿出來(即取出集合所有元素并進(jìn)行操作)

public static void getOutFruits(List<Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}

接著在裝水果的藍(lán)子(List<Fruit>)和裝蘋果的籃子(List<Apple>)上執(zhí)行這個(gè)方法:

List<Fruit> fruitBasket = new ArrayList<Fruit>();
fruitBasket(new Fruit());
getOutFruits(fruitBasket);//成功

List<Apple> appleBasket = new ArrayList<Apple>();
appleBasket(new Apple());
//getOutFruits(appleBasket);//編譯錯(cuò)誤
//getOutFruits((List<Fruit>) appleBasket);//強(qiáng)制類型轉(zhuǎn)換耙册,同樣編譯錯(cuò)誤
//不兼容的類型: List<Apple>無法轉(zhuǎn)換為L(zhǎng)ist<Fruit>

結(jié)果出人意料:裝蘋果的籃子(List<Apple>)執(zhí)行時(shí)編譯出錯(cuò)了给僵。錯(cuò)誤顯示無法轉(zhuǎn)換。強(qiáng)制轉(zhuǎn)換也沒有用详拙。

這個(gè)不科學(xué)呀! 在面向?qū)ο笾械奂剩宇愋蛯?duì)象是可以轉(zhuǎn)成父類型的蔓同。

這不科學(xué)

原來泛型是不可變。即對(duì)于任何2個(gè)不同類型的type1和type2胡本,List<Type1>即不是List<Type2>的子類型牌柄,也不是List<Type2>的超類型。(《effective java》第25條 )

所以侧甫,F(xiàn)ruit和Apple雖是父子關(guān)系珊佣,但作為2個(gè)不同的類型,List<Apple>和List<Fruit>之間沒有繼承關(guān)系披粟,所以2者之間無法轉(zhuǎn)化咒锻。

使用<? extends T>進(jìn)行改進(jìn)

如果想解決上面的問題,即在裝水果的藍(lán)子(List<Fruit>)的地方守屉,兼容裝蘋果的籃子(List<Apple>)惑艇,則需要使用<? extends T>這種通配符泛型。

/**參數(shù)使用List<? extends Fruit>**/
public static void getOutFruits(List<? extends Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}
public static void main(String[] args) {
    List<Fruit> fruitBasket = new ArrayList<>();
    fruitBasket.add(new Fruit());
    getOutFruits(fruitBasket);

    List<Apple> appleBasket = new ArrayList<>();
    appleBasket.add(new Apple());
    getOutFruits(appleBasket);//編譯正確
}

問題解決了拇泛。說明List<? extends Fruit>滨巴,同時(shí)兼容了List<Fruit>和List<Apple>,我們可以理解為L(zhǎng)ist<? extends Fruit>現(xiàn)在是List<Fruit>和List<Apple>的超類型(父類型)了

哎呦俺叭,原來使用<? extends T>就萬事大吉恭取,哈哈!
怎么可能熄守?少年你還太年輕了蜈垮!

再看這個(gè)例子

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? extends Fruit> basket = apples;//按上一個(gè)例子,這個(gè)是可行的
for (Fruit fruit : basket)
{
    System.out.println(fruit);
}

//basket.add(new Apple()); //編譯錯(cuò)誤
//basket.add(new Fruit()); //編譯錯(cuò)誤

問題出現(xiàn)了裕照,明明是就放水果的籃子(List<? extends Fruit>攒发,可兼容List<Fruit>和List<Apple>),現(xiàn)在不僅不能放蘋果到里面晋南,連水果也不能放入了惠猿。不過從籃子取出水果是可以的,這又是怎么回事负间?

筆者試著用解釋一下:用了<? extends Fruit>相當(dāng)于告訴編譯器偶妖,我們的籃子(集合)是用來處理水果以及水果的子類型。因?yàn)樽宇愋陀性S多唉擂,我們并沒有告訴編譯器是哪個(gè)子類型。

編譯器在這里遇到的問題是檀葛,如果add的是Apple類型時(shí)玩祟,則basket應(yīng)該是List<Apple>,如果add是Fruit類型屿聋,則basket應(yīng)該是List<Fruit>空扎。而List<Apple>和List<Fruit>前面已經(jīng)提過藏鹊,是2個(gè)完全沒有關(guān)系的類型,
所以編譯器不知道是哪個(gè)子類型將加入集合转锈,不知道到底是List<Apple>還是List<Fruit>盘寡,所以編譯器只能報(bào)錯(cuò)。(注意撮慨,這里討論的都是類型竿痰,而不是對(duì)象)

另一方面,編譯器已經(jīng)知道集合里全部都是水果的子類型砌溺,所以編譯器可以保證取出的數(shù)據(jù)全部是水果影涉。

所以,在上面的例子中规伐,我們從籃子中拿水果蟹倾,實(shí)際就是從集合里獲取元素。簡(jiǎn)單的說猖闪,當(dāng)只想從集合中獲取元素鲜棠,請(qǐng)把這個(gè)集合看成生產(chǎn)者,請(qǐng)使用<? extends T>培慌,這就是Producer extends原則豁陆,PECS原則中的PE部分。

改用<? super T>試試

上一個(gè)例子里检柬,我們不能往籃子里加水果∠琢現(xiàn)在換一個(gè)角度,我們要實(shí)現(xiàn)如何往籃子里加水果何址,而且是不同的水果里逆。這將用到<? super T>通配符泛型。

首先我們擴(kuò)展一下水果的繼承關(guān)系用爪,增加蘋果的子類型redApple:

public class Fruit {...}
public class Apple extends Fruit {...}
public class RedApple extends Apple {...}

下面使用<? super T>的例子:

List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? super Apple> basket = apples;//這里使用了super

basket.add(new Apple());
basket.add(new RedApple());
//basket.add(new Fruit()); //編譯錯(cuò)誤

Object object = basket.get(0);//正確
//Fruit fruit =basket.get(0);//編譯錯(cuò)誤
//Apple apple = basket.get(0);//編譯錯(cuò)誤
//RedApple redApple = basket.get(0);//編譯錯(cuò)誤

顯然原押,蘋果和紅萍果都能正確地放入籃子(List<? super Apple>)。但奇怪的是偎血,水果對(duì)象卻不能诸衔。另一個(gè)奇怪現(xiàn)象是,籃子中只能取出Object類型的對(duì)象颇玷。

筆者試圖解釋一下:用了<? super Apple>相當(dāng)于告訴編譯器笨农,集合接受處理Apple以及Apple的超類型,即Object帖渠,F(xiàn)ruit谒亦,Apple三個(gè)類型。
但編譯器并不知道到底是List<Object>,List<Fruit>還是List<Apple>份招?

編譯器只知道切揭,蘋果和蘋果子類型是可以放進(jìn)去(也是Fruit的子類型,也是Object的子類型)锁摔。這意味著廓旬,我們總是可以將一個(gè)蘋果的子類型放入蘋果的超類型的list中。

而取出時(shí)的情況是谐腰,編譯器不知道是按哪個(gè)類型取出孕豹, 到底是Object,F(xiàn)ruit怔蚌,Apple中的哪個(gè)呢巩步?但是編譯器可以選擇永遠(yuǎn)不會(huì)錯(cuò)的類型,也就是Object的類型桦踊,因?yàn)镺bject是所有類型的超類型椅野。

因此,在上面的例子中的籍胯,我們將數(shù)據(jù)放進(jìn)集合List<? super Apple> basket竟闪,所以這個(gè)籃子是實(shí)際上消費(fèi)元素,例如Apple杖狼。簡(jiǎn)單的說炼蛤,當(dāng)你僅僅想增加元素到集合,把這個(gè)集合看成消費(fèi)者蝶涩,請(qǐng)使用<? super T>理朋。這就是Consumer super原則,PECS原則中的CS部分绿聘。

總結(jié)PECS原則

  • 如果你只需要從集合中獲得類型T , 使用<? extends T>通配符
  • 如果你只需要將類型T放到集合中, 使用<? super T>通配符
  • 如果你既要獲取又要放置元素嗽上,則不使用任何通配符。例如List<Apple>
  • PECS即 Producer extends Consumer super熄攘, 為了便于記憶兽愤。(《effective java》第28條)

為何要PECS原則?

你還記得前面提到泛型是不可變嗎挪圾?即List<Fruit>和List<Apple>之間沒有任何繼承關(guān)系浅萧。API的參數(shù)想要同時(shí)兼容2者,則只能使用PECS原則哲思。這樣做提升了API的靈活性洼畅。
在java集合API中,大量使用了PECS原則棚赔,例如java.util.Collections中的集合復(fù)制的方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  ...
}

集合復(fù)制是最典型的用法:

  • 復(fù)制源集合src帝簇,主要獲得元素务热,所以用<? extends T>
  • 復(fù)制目標(biāo)集合dest,主要是設(shè)置元素己儒,所以用<? super T>

當(dāng)然,為了提升了靈活性捆毫,自然犧牲了部分功能闪湾。魚和熊掌不能兼得。

補(bǔ)充說明

  • 這里的錯(cuò)誤全部是編譯階段不是運(yùn)行階段绩卤,編譯階段程序是沒有運(yùn)行途样。所以不能用運(yùn)行程序的思維來思考。
  • 使用泛型濒憋,就是要在編譯階段何暇,就找出類型的錯(cuò)誤來。

參考資料

《Effective Java》第2版

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凛驮,一起剝皮案震驚了整個(gè)濱河市裆站,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌黔夭,老刑警劉巖宏胯,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異本姥,居然都是意外死亡肩袍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門婚惫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來氛赐,“玉大人,你說我怎么就攤上這事先舷〖韫埽” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵密浑,是天一觀的道長(zhǎng)蛙婴。 經(jīng)常有香客問我,道長(zhǎng)尔破,這世上最難降的妖魔是什么街图? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮懒构,結(jié)果婚禮上餐济,老公的妹妹穿的比我還像新娘。我一直安慰自己胆剧,他們只是感情好絮姆,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布醉冤。 她就那樣靜靜地躺著,像睡著了一般篙悯。 火紅的嫁衣襯著肌膚如雪蚁阳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天鸽照,我揣著相機(jī)與錄音螺捐,去河邊找鬼。 笑死矮燎,一個(gè)胖子當(dāng)著我的面吹牛定血,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诞外,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼澜沟,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了峡谊?” 一聲冷哼從身側(cè)響起茫虽,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎既们,沒想到半個(gè)月后席噩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贤壁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年悼枢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脾拆。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡馒索,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出名船,到底是詐尸還是另有隱情绰上,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布渠驼,位于F島的核電站蜈块,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏迷扇。R本人自食惡果不足惜百揭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜓席。 院中可真熱鬧器一,春花似錦、人聲如沸厨内。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至请毛,卻和暖如春志鞍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背方仿。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工述雾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人兼丰。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像唆缴,于是被迫代替她去往敵國(guó)和親鳍征。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容