本文通過一個(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)成父類型的蔓同。
原來泛型是不可變。即對(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版