泛型上下界、萬用字元介紹
<? extends T>
和<? super T>
的英文Java的泛型中的“萬用字元(萬用字元)”和“邊界(邊界)”的概念。
- <?擴充套件T>:是指 “上界萬用字元(Upper Bounds Wildcards)”
- <?super T>:是指 “下界萬用字元(Lower Bounds Wildcards)”
為什麼要用萬用字元和邊界?
使用泛型的過程中,經常出現一種很彆扭的情況。比如按照題主的例子,我們有水果類,和它的派生類蘋果類。
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裝水果的盤子
所以,就算容器裡裝的東西之間有繼承關係,但容器之間是沒有繼承關係的。所以我們不可以把板的引用傳遞給板。
為了讓泛型用起來更舒服,太陽大的腦袋們就想出了<? extends T>
<? super T>
的辦法,來讓”水果盤子‘和’蘋果盤子“之間發生關係。
什麼是上界?
下面程式碼就是“上界萬用字元(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>
覆蓋下圖產品中藍色的區域。
什麼是下界?
相對應的,“下界萬用字元(Lower Bounds Wildcards)”:
Plate<? super Fruit>
表達的就是相反的概念:一個能放水果以及一切是水果基類的盤子。Plate<? super Fruit>
是Plate<Fruit>
的基類,但不是Plate<Apple>
的基類。對應剛才那個例子,Plate<? super Fruit>
覆蓋下圖中紅色的區域。
上下界萬用字元的副作用
邊界讓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;}
}
上界<? 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賦值以後,盤子裡沒有被標上有“蘋果”。而是標上一個佔位符:CAP#1,來表示捕獲一個Fruit或Fruit的子類,具體是什麼類不知道,代號CAP#1。然後無論是想往裡插入Apple或者Meat或者Fruit編譯器都不知道能不能和這個CAP#1匹配,所以就都不允許。
萬用字元<?>和型別引數的區別就在於,對編譯器來說所有的T都代表同一種類型。比如下面這個泛型方法裡,三個T都指代同一個型別,要麼都是String,要麼都是Integer。
public <T> List<T> fill(T... t);
但萬用字元<?>
沒有這種約束,Plate<?>
單純的就表示:盤子裡放了一個東西,是什麼我不知道。
所以題主問題裡的錯誤就在這裡,Plate<? extends Fruit>
裡什麼都放不進去。
下界<? 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物件才能裝下。但這樣的話,元素的型別資訊就全部丟失。
PECS原則
最後看一下什麼是PECS(Producer Extends Consumer Super)原則,已經很好理解了:
- 頻繁往外讀取內容的,適合用上界Extends。
- 經常往裡插入的,適合用下界Super。
總結
extends 可用於返回型別限定,不能用於引數型別限定(換句話說:? extends xxx 只能用於方法返回型別限定,jdk能夠確定此類的最小繼承邊界為xxx,只要是這個類的父類都能接收,但是傳入引數無法確定具體型別,只能接受null的傳入)。
super 可用於引數型別限定,不能用於返回型別限定(換句話說:? supper xxx 只能用於方法傳參,因為jdk能夠確定傳入為xxx的子類,返回只能用Object類接收)。
? 既不能用於方法引數傳入,也不能用於方法返回。
經常發現有List<? super T>、Set<? extends T>的宣告,是什麼意思呢?
<? super T>表示包括T在內的任何T的父類,<? extends T>表示包括T在內的任何T的子類;
下面我們詳細分析一下兩種萬用字元具體的區別。
擴充套件
萬用字元與T的區別
T:作用於模板上,用於將資料型別進行引數化,不能用於例項化物件。
?:在例項化物件的時候,不確定泛型引數的具體型別時,可以使用萬用字元進行物件定義。
< T > 等同於 < T extends Object>
< ? > 等同於 < ? extends Object>
<? extends T>:是指 “上界萬用字元(Upper Bounds Wildcards)”
<? super T>:是指 “下界萬用字元(Lower Bounds Wildcards)”
?extends T>限定引數型別的上界:引數型別必須是T或T的子型別
<?super T>限定引數型別的下界:引數型別必須是T或T的超型別