extends T>和 extends > 是Java泛型中的“通配符(Wildcards)”和“邊界(Bounds)”的概念。
extends T>:是指“上界通配符(Upper Bounds Wildcards”
extends >: 是指“下界通配符(Lower Bounds Wildcards)”
1. 為什麼要用通配符和邊界?
使用泛型的過程中,經常出現一種很别扭的情況。比如我們有Fruit類和它的派生類Apple類。
class Fruit { }
class Apple extends Fruit { }
然後有一個最簡單的容器:Plate類。盤子裡可以放一個泛型的“東西”。我們可以對這個東西做最簡單的“放”和“取”的動作:set()和get()方法。
class Plate {
private T item;
public Plate(T t) {
item = t;
}
public void set(T t) {
item = t;
}
public T get() {
return item;
}
}
現在我們可以定義一個“水果盤子”,邏輯上水果盤子當然可以裝蘋果。
Plate p = new Plate(new Apple());
但實際上Java編譯器不允許這個操作,會報錯。
Type mismatch: cannot convert from Plate to Plate
實際上,編譯器認定的邏輯是這樣的:
蘋果is-a水果
裝蘋果的盤子not-is-a裝水果的盤子
是以,就算容器裡裝的東西之間有繼承關系,但容器之間是沒有繼承關系的。是以我們不可以把Plate的引用傳遞給Plate。
為了讓泛型用起來更舒服,于是就有了 extends T>和< super T>的辦法,來讓“水果盤子”和“蘋果盤子”有聯系。
2 什麼是上界
下面代碼就是“上界通配符”:
Plate extends Fruit>
意思是:一個能放水果以及一切是水果派生類的盤子。這和我們人類的邏輯就比較接近了。Plate extends Fruit>和Plate最大的差別就是:Plate extends Fruit>是Plate以及Plate的基類。直接的好處就是,我們可以用“蘋果盤子”給“水果盤子”指派。
Plate extends Fruit> p = new Plate(new Apple());
如果把Fruit和Apple的例子再擴充一下,食物分成水果和肉類,水果還有蘋果和香蕉,肉類有豬肉和牛肉,蘋果還有兩種青蘋果和紅蘋果。
在這個繼承體系中,下界通配符Plate extends Fruit> 覆寫下圖中藍色的區域。
繼承體系圖
3. 什麼是下界
相應的,“下界通配符”:
Plate super Fruit>
表達的就是相反的概念:一個能放水果以及一切是水果基類的盤子。Plate<?super Fruit>是Plate的基類,但不是Plate的基類。對應剛才那個例子,Plate super Fruit>覆寫下圖中紅色的區域:
繼承體系圖
4. 上下界通配符的副作用
邊界讓Java不同泛型之間的轉換更容易了。但不要忘記,這樣的轉換也有一定的副作用。那就是容器的部分功能可能失效。
還是以剛才的Plate為例。我們可以對盤子做兩件事,往盤子裡set( )新東西,以及從盤子裡get()東西。
class Plate {
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(new Apple());
//不能存入任何元素
p.set(new Fruit());//Error
p.set(new Apple());//Error
//讀取出來的東西隻能存放在Fruit或它的基類裡
Fruit other1 = p.get();
Object other2 = p.get();
Apple other3 = 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 List fill(T... t);
但通配符>沒有這種限制,Plate>單純的就表示:盤子裡放了一個東西,是什麼我不知道。
是以Plate裡什麼都放不進去。
4.2 下界 super T>不影響往裡存,但往外取隻能放在Object對象裡
Plate super Fruit> p = new Plate(new Apple());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//讀取出來的東西隻能存放在Object類裡
Fruit other1 = p.get();//Error
Apple other2 = p.get();//Error
Object other3 = p.get();
使用下界 super Fruit>會使從盤子裡取東西的get( )方法部分失效,隻能存放到Object對象裡。set( )方法正常。
因為下界規定了元素的最小粒度的下限,實際上是放松了容器元素的類型控制。既然元素是Fruit的基類,那往裡存粒度比Fruit小的都可以。但往外讀取元素就費勁了,隻有所有類的基類Object對象才能裝下。但這樣的話,元素的類型資訊就全部丢失。
5. PECS原則
1) T super B>
對于這個泛型,?代表容器裡的元素類型,由于隻規定了元素必須是B的超類,導緻元素沒有明确統一的“根”(除了Object這個必然的根),是以這個泛型你其實無法使用它,對吧,除了把元素強制轉成Object。是以,對把參數寫成這樣形态的函數,你函數體内,隻能對這個泛型做插入操作,而無法讀
2) T extends B>
由于指定了B為所有元素的“根”,你任何時候都可以安全的用B來使用容器裡的元素,但是插入有問題,由于供奉B為祖先的子樹有很多,不同子樹并不相容,由于實參可能來自于任何一顆子樹,是以你的插入很可能破壞函數實參,是以,對這種寫法的形參,禁止做插入操作,隻做讀取
最後看一下什麼是PECS(Producer Extends Consumer Super)原則,已經很好了解了:
頻繁往外讀取内容的,适合用上界Extends。
經常往裡插入的,适合用下界Super。