Java 泛型解惑之 extends T>和 super T>
1 為什麼要用萬用字元和邊界?
使用泛型的過程中,經常出現一種很彆扭的情況
比如我們有 Fruit 類,和它的派生類 Apple
class Fruit {} class Apple extends Fruit {}
然後有一個最簡單的容器: Plate 類
盤子裡可以放一個泛型的“ 東西 ”
我們可以對這個東西做最簡單的“ 放 ”和“ 取 ”的動作: set( ) 和 get( ) 方法
class Plate<T>{ private T item; public Plate(T t){item=t;} public void set(T t){item=t;} public T get(){return item;} }
現定義一個“ 水果盤 ”,邏輯上水果盤當然可以裝蘋果
Plate<Fruit> p = new Plate<Apple>(new Apple());
但實際上Java編譯器不允許這個操作。會報錯,“ 裝蘋果的盤子 ”無法轉換成“ 裝水果的盤子 ”。
error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>
實際上,編譯器認定的邏輯是這樣的:
- 蘋果 IS-A 水果
- 裝蘋果的盤子 NOT-IS-A 裝水果的盤子
所以,就算容器裡裝的東西之間有繼承關係,但 容器之間是沒有繼承關係
所以我們不可以把 Plate<Apple> 的引用傳遞給 Plate<Fruit>
為了讓泛型用起來更舒服,Sun的大師們就想出了<? extends T>和<? super T>的辦法,來讓” 水果盤子 “和” 蘋果盤子 “之間發生正當關係
2 上界
下面就是上界萬用字元(Upper Bounds Wildcards)
Plate<? extends Fruit>
一個能放水果以及一切是水果派生類的盤子
啥水果都能放的盤子
這和我們人類的邏輯就比較接近了
Plate<? extends Fruit>和Plate<Apple>最大的區別就是:
Plate<? extends Fruit>是Plate<Fruit>及Plate<Apple>的基類直接的好處就是,我們可以用“ 蘋果盤 ”給“
水果盤”賦值了Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
再擴充套件一下,食物分成水果和肉類,水果有蘋果和香蕉,肉類有豬肉和牛肉,蘋果還有兩種青蘋果和紅蘋果
//Lev 1 class Food{} //Lev 2 class Fruit extends Food{} class Meat extends Food{} //Lev 3 class Apple extends Fruit{} class Banana extends Fruit{} class Pork extends Meat{} class Beef extends Meat{} //Lev 4 class RedApple extends Apple{} class GreenApple extends Apple{}
在這個體系中,上界萬用字元 Plate<? extends Fruit>
覆蓋下圖中藍色的區域

3 下界
相對應的下界萬用字元(Lower Bounds Wildcards)
Plate<? super Fruit>
表達的就是相反的概念: 一個能放水果以及一切是水果基類的盤子
Plate<? super Fruit>是Plate<Fruit>的基類,但不是Plate<Apple>的基類
對應剛才那個例子,Plate<? super Fruit>覆蓋下圖中紅色的區域。

4 上下界萬用字元的副作用
邊界讓Java不同泛型之間的轉換更容易了。但不要忘記,這樣的轉換也有一定的副作用。那就是容器的部分功能可能失效。
還是以剛才的Plate為例。我們可以對盤子做兩件事,往盤子裡set( )新東西,以及從盤子裡get( )東西
class Plate<T>{ private T item; public Plate(T t){item=t;} public void set(T t){item=t;} public T get(){return item;} }
4.1 上界<? extends T>不能往裡存,只能往外取
<? extends Fruit>會使往盤子裡放東西的set( )方法失效
但取東西get( )方法還有效
比如下面例子裡兩個set()方法,插入Apple和Fruit都報錯
Plate<? extends Fruit> p = new Plate<Apple>(new Apple()); //不能存入任何元素 p.set(new Fruit());//Error p.set(new Apple());//Error //讀取出來的東西只能存放在Fruit或它的基類裡。 Fruit newFruit1=p.get(); Object newFruit2=p.get(); Apple newFruit3=p.get();//Error
Fruit或者它的派生類
,但
具體是什麼型別不知道
可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?編譯器在看到後面用Plate<Apple>賦值以後,盤子裡沒有被標上有“蘋果”。而是標上一個佔位符: capture#1 ,來表示捕獲一個Fruit或Fruit的子類,具體是什麼類不知道,代號capture#1
然後無論是想往裡插入Apple或者Meat或者Fruit編譯器都不知道能不能和這個capture#1匹配,所以就都不允許
所以萬用字元<?>和型別引數<T>的區別就在於,對編譯器來說 所有的T都代表同一種類型
比如下面這個泛型方法裡,三個T都指代同一個型別,要麼都是String,要麼都是Integer...
public <T> List<T> fill(T... t);
但萬用字元<?>沒有這種約束,Plate<?>單純的就表示: 盤子裡放了一個東西,是什麼我不知道
4.2 下界<? super T>不影響往裡存,但往外取只能放在Object物件裡
使用下界<? super Fruit>會使從盤子裡取東西的get( )方法部分失效,只能存放到Object物件裡。set( )方法正常。
Plate<? super Fruit> p=new Plate<Fruit>(new Fruit()); //存入元素正常 p.set(new Fruit()); p.set(new Apple()); //讀取出來的東西只能存放在Object類裡。 Apple newFruit3=p.get();//Error Fruit newFruit1=p.get();//Error Object newFruit2=p.get();
因為下界規定了元素的最小粒度的下限,實際上是放鬆了容器元素的型別控制
既然元素是Fruit的基類,那往裡存粒度比Fruit小的都可以
但往外讀取元素就費勁了,只有所有類的基類Object物件才能裝下。但這樣的話, 元素的型別資訊就全部丟失
5 PECS原則
最後看一下什麼是 PECS(Producer Extends Consumer Super)原則 ,已經很好理解了
- 頻繁往外讀取內容的,適合用上界Extends
- 經常往裡插入的,適合用下界Super