<? extends T>和<? super T>是Java泛型中的“通配符(Wildcards)”和“邊界(Bounds)”的概念。
- <? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”
- <? super T>:是指 “下界通配符(Lower Bounds Wildcards)”
為什么要用通配符和邊界?--泛型不是協(xié)變的
開發(fā)人員在使用泛型的時候,很容易根據(jù)自己的直覺而犯一些錯誤。比如一個方法如果接收 List<Object> 作為形式參數(shù),那么如果嘗試將一個 List<String> 的對象作為實(shí)際參數(shù)傳進(jìn)去,卻發(fā)現(xiàn)無法通過編譯。
雖然從直覺上來說,Object 是 String 的父類,這種類型轉(zhuǎn)換應(yīng)該是合理的。但是實(shí)際上這會產(chǎn)生隱含的類型轉(zhuǎn)換問題,因此編譯器直接就禁止這樣的行為。
比如我們有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;}
}
現(xiàn)定義一個“水果盤”,邏輯上水果盤當(dāng)然可以裝蘋果。
Plate<Fruit> p=new Plate<Apple>(new Apple());
但實(shí)際上Java編譯器不允許這個操作。會報錯,“裝蘋果的盤子”無法轉(zhuǎn)換成“裝水果的盤子”。
error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>
實(shí)際上,編譯器認(rèn)定的邏輯是這樣的:
蘋果 IS-A 水果
裝蘋果的盤子 NOT-IS-A 裝水果的盤子
所以,就算容器里裝的東西之間有繼承關(guān)系,但容器之間是沒有繼承關(guān)系。
所以我們不可以把Plate<Apple>的引用傳遞給Plate<Fruit>。
泛型不是協(xié)變的
在 Java 語言中,數(shù)組是協(xié)變的,也就是說,如果 Integer 擴(kuò)展了 Number,那么不僅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以傳遞或者賦予 Integer[]。(更正式地說,如果 Number是 Integer 的超類型,那么 Number[] 也是 Integer[]的超類型)。
您也許認(rèn)為這一原理同樣適用于泛型類型 —— List< Number> 是 List< Integer> 的超類型,那么可以在需要 List< Number> 的地方傳遞 List< Integer>。不幸的是,情況并非如此。為啥呢?這么做將破壞要提供的類型安全泛型。
類型擦除
正確理解泛型概念的首要前提是理解類型擦除(type erasure)。 Java 中的泛型基本上都是在編譯器這個層次來實(shí)現(xiàn)的。在生成的 Java 字節(jié)代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數(shù),會被編譯器在編譯的時候去掉。這個過程就稱為類型擦除。如在代碼中定義的 List<Object> 和 List<String> 等類型,在編譯之后都會變成 List。JVM 看到的只是 List,而由泛型附加的類型信息對 JVM 來說是不可見的。Java 編譯器會在編譯時盡可能的發(fā)現(xiàn)可能出錯的地方,但是仍然無法避免在運(yùn)行時刻出現(xiàn)類型轉(zhuǎn)換異常的情況。類型擦除也是 Java 的泛型實(shí)現(xiàn)方式與C++ 模板機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別。
public class Test {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass().getName());
System.out.println(intList.getClass().getName());
}
}
上面這一段代碼,運(yùn)行后輸出如下,可知在運(yùn)行時獲取的類型信息是不帶具體類型的:
java.util.ArrayList
java.util.ArrayList
很多泛型的奇怪特性都與這個類型擦除的存在有關(guān),包括:
泛型類并沒有自己獨(dú)有的 Class 類對象。比如并不存在 List<String>.class 或是 List<Integer>.class,而只有 List.class,因此在運(yùn)行時無法獲得泛型的真實(shí)類型信息。
靜態(tài)變量是被泛型類的所有實(shí)例所共享的。對于聲明為 MyClass<T> 的類,訪問其中的靜態(tài)變量的方法仍然是 MyClass.myStaticVar。不管是通過 new MyClass<String> 還是 new MyClass<Integer> 創(chuàng)建的對象,都是共享一個靜態(tài)變量。
泛型的類型參數(shù)不能用在 Java 異常處理的 catch 語句中。因?yàn)楫惓L幚硎怯?JVM 在運(yùn)行時刻來進(jìn)行的。由于類型信息被擦除,JVM 是無法區(qū)分兩個異常類型 MyException<String> 和 MyException<Integer> 的。對于 JVM 來說,它們都是 MyException 類型的。也就無法執(zhí)行與異常對應(yīng)的 catch 語句。
類型擦除的基本過程也比較簡單,首先是找到用來替換類型參數(shù)的具體類。這個具體類一般是 Object。如果指定了類型參數(shù)的上界的話,則使用這個上界。把代碼中的類型參數(shù)都替換成具體的類。同時去掉出現(xiàn)的類型聲明,即去掉 <> 的內(nèi)容。比如 T get() 方法聲明就變成了 Object get();List<String> 就變成了 List。接下來就可能需要生成一些橋接方法(bridge method)。這是由于擦除了類型之后的類可能缺少某些必須的方法。比如考慮下面的代碼:
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
當(dāng)類型信息被擦除之后,上述類的聲明變成了 class MyString implements Comparable。但是這樣的話,類 MyString 就會有編譯錯誤,因?yàn)闆]有實(shí)現(xiàn)接口 Comparable 聲明的 int compareTo(Object) 方法。這個時候就由編譯器來動態(tài)生成這個方法。
實(shí)例分析
了解了類型擦除機(jī)制之后,就會明白編譯器承擔(dān)了全部的類型檢查工作。編譯器禁止某些泛型的使用方式,正是為了確保類型的安全性。以上面提到的 List<Object> 和 List<String> 為例來具體分析:
public void inspect(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(1); // 這個操作在當(dāng)前方法的上下文是合法的。
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); // 編譯錯誤
}
這段代碼中,inspect 方法接受 List<Object> 作為參數(shù),當(dāng)在 test 方法中試圖傳入 List<String> 的時候,會出現(xiàn)編譯錯誤。
假設(shè)這樣的做法是允許的,那么在 inspect 方法就可以通過 list.add(1) 來向集合中添加一個數(shù)字。這樣在 test 方法看來,其聲明為 List<String> 的集合中卻被添加了一個 Integer 類型的對象。這顯然是違反類型安全的原則的,在某個時候肯定會拋出ClassCastException。因此,編譯器禁止這樣的行為。
編譯器會盡可能的檢查可能存在的類型安全問題。對于確定是違反相關(guān)原則的地方,會給出編譯錯誤。當(dāng)編譯器無法判斷類型的使用是否正確的時候,會給出警告信息。
為了讓泛型用起來更舒服,Sun的大師們就想出了<? extends T>和<? super T>的辦法,來讓”水果盤子“和”蘋果盤子“之間發(fā)生正當(dāng)關(guān)系。
通配符
在使用泛型類的時候,
既可以指定一個具體的類型,如 List<String> 就聲明了具體的類型是 String;
也可以用通配符? 來表示未知類型,如 List<?> 就聲明了 List 中包含的元素類型是未知的。
通配符所代表的其實(shí)是一組類型,但具體的類型是未知的。List<?> 所聲明的就是所有類型都是可以的。但是 List<?> 并不等同于 List<Object>。
List<Object> 實(shí)際上確定了 List 中包含的是 Object 及其子類,在使用的時候都可以通過 Object 來進(jìn)行引用。而 List<?> 則其中所包含的元素類型是不確定。其中可能包含的是 String,也可能是 Integer。如果它包含了 String 的話,往里面添加 Integer 類型的元素就是錯誤的。正因?yàn)轭愋臀粗?,就不能通過 new ArrayList<?>() 的方法來創(chuàng)建一個新的 ArrayList 對象。因?yàn)榫幾g器無法知道具體的類型是什么。但是對于 List<?> 中的元素確總是可以用 Object 來引用的,因?yàn)殡m然類型未知,但肯定是 Object 及其子類??紤]下面的代碼:
public void wildcard(List<?> list) {
list.add(1);// 編譯錯誤
}
如上所示,試圖對一個帶通配符的泛型類進(jìn)行操作的時候,總是會出現(xiàn)編譯錯誤。其原因在于通配符所表示的類型是未知的。
這就是三句話總結(jié)JAVA泛型通配符(PECS)中的第一句話:?”不能添加元素,只能作為消費(fèi)者
因?yàn)閷τ?List<?> 中的元素只能用 Object 來引用,在有些情況下不是很方便。在這些情況下,可以使用上下界來限制未知類型的范圍。 如 List<? extends Number> 說明 List 中可能包含的元素類型是 Number 及其子類。而 List<? super Number> 則說明 List 中包含的是 Number 及其父類。當(dāng)引入了上界之后,在使用類型的時候就可以使用上界類中定義的方法。比如訪問 List<? extends Number> 的時候,就可以使用 Number 類的 intValue 等方法。
類型系統(tǒng)
在 Java 中,大家比較熟悉的是通過繼承機(jī)制而產(chǎn)生的類型體系結(jié)構(gòu)。比如 String 繼承自 Object。根據(jù)Liskov 替換原則,子類是可以替換父類的。當(dāng)需要 Object 類的引用的時候,如果傳入一個 String 對象是沒有任何問題的。但是反過來的話,即用父類的引用替換子類引用的時候,就需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換。編譯器并不能保證運(yùn)行時刻這種轉(zhuǎn)換一定是合法的。這種自動的子類替換父類的類型轉(zhuǎn)換機(jī)制,對于數(shù)組也是適用的(數(shù)組是協(xié)變的)。 String[] 可以替換 Object[]。但是泛型的引入,對于這個類型系統(tǒng)產(chǎn)生了一定的影響。正如前面提到的 List<String> 是不能替換掉 List<Object> 的。
引入泛型之后的類型系統(tǒng)增加了兩個維度:一個是類型參數(shù)自身的繼承體系結(jié)構(gòu),另外一個是泛型類或接口自身的繼承體系結(jié)構(gòu)。第一個指的是對于 List<String> 和 List<Object> 這樣的情況,類型參數(shù) String 是繼承自 Object 的。而第二種指的是 List 接口繼承自 Collection 接口。對于這個類型系統(tǒng),有如下的一些規(guī)則:
相同類型參數(shù)的泛型類的關(guān)系取決于泛型類自身的繼承體系結(jié)構(gòu)。即 List<String> 是 Collection<String> 的子類型,List<String> 可以替換 Collection<String>。這種情況也適用于帶有上下界的類型聲明。
當(dāng)泛型類的類型聲明中使用了通配符的時候, 其子類型可以在兩個維度上分別展開。如對 Collection<? extends Number> 來說,其子類型可以在 Collection 這個維度上展開,即 List<? extends Number> 和 Set<? extends Number> 等;也可以在 Number 這個層次上展開,即 Collection<Double> 和 Collection<Integer> 等。如此循環(huán)下去,ArrayList<Long> 和 HashSet<Double> 等也都算是 Collection<? extends Number> 的子類型。
如果泛型類中包含多個類型參數(shù),則對于每個類型參數(shù)分別應(yīng)用上面的規(guī)則。
理解了上面的規(guī)則之后,就可以很容易的修正實(shí)例分析中給出的代碼了。只需要把 List<Object> 改成 List<?> 即可。List<String> 是 List<?> 的子類型,因此傳遞參數(shù)時不會發(fā)生錯誤。
上界
下面就是上界通配符(Upper Bounds Wildcards)
Plate<? extends Fruit>
一個能放水果以及一切是水果派生類的盤子
再直白點(diǎn)就是:啥水果都能放的盤子,這和我們?nèi)祟惖倪壿嬀捅容^接近了
Plate<? extends Fruit>和Plate<Apple>最大的區(qū)別就是:Plate<? extends Fruit>是Plate<Fruit>及Plate<Apple>的基類
直接的好處就是,我們可以用“蘋果盤”給“水果盤”賦值了。
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
再擴(kuò)展一下,食物分成水果和肉類,水果有蘋果和香蕉,肉類有豬肉和牛肉,蘋果還有兩種青蘋果和紅蘋果。
//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>覆蓋下圖中藍(lán)色的區(qū)域。

下界
相對應(yīng)的下界通配符(Lower Bounds Wildcards)
Plate<? super Fruit>
表達(dá)的就是相反的概念:一個能放水果以及一切是水果基類的盤子。
Plate<? super Fruit>是Plate<Fruit>的基類,但不是Plate<Apple>的基類
對應(yīng)剛才那個例子,Plate<? super Fruit>覆蓋下圖中紅色的區(qū)域。

上下界通配符的副作用
邊界讓Java不同泛型之間的轉(zhuǎn)換更容易了。但不要忘記,這樣的轉(zhuǎn)換也有一定的副作用。那就是容器的部分功能可能失效。
還是以剛才的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;}
}
1、上界<? extends T>不能往里存,只能往外取
(1).<? extends Fruit>會使往盤子里放東西的set( )方法失效,但取東西get( )方法還有效
(2).取出來的東西只能存放在Fruit或它的基類里面,向上造型。
比如下面例子里兩個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
編譯器只知道容器內(nèi)是Fruit或者它的派生類,但具體是什么類型不知道,因此取出來的時候要向上造型為基類。
可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?編譯器在看到后面用Plate<Apple>賦值以后,盤子里沒有被標(biāo)上有“蘋果”。而是標(biāo)上一個占位符:capture#1,來表示捕獲一個Fruit或Fruit的子類,具體是什么類不知道,代號capture#1。
然后無論是想往里插入Apple或者M(jìn)eat或者Fruit編譯器都不知道能不能和這個capture#1匹配,所以就都不允許。
所以通配符<?>和類型參數(shù)<T>的區(qū)別就在于,對編譯器來說所有的T都代表同一種類型。
比如下面這個泛型方法里,三個T都指代同一個類型,要么都是String,要么都是Integer...
public <T> List<T> fill(T... t);
但通配符<?>沒有這種約束,Plate<?>單純的就表示:盤子里放了一個東西,是什么我不知道。
2、下界<? super T>不影響往里存,但往外取只能放在Object對象里
- (1).使用下界<? super Fruit>會使從盤子里取東西的get( )方法部分失效,只能存放到Object對象里。
因?yàn)橐?guī)定的下界,對于上界并不清楚,所以只能放到最根本的基類Object中
- (2).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();
因?yàn)橄陆缫?guī)定了元素的最小粒度的下限,實(shí)際上是放松了容器元素的類型控制。
既然元素是Fruit的基類,那往里存粒度比Fruit小的都可以。
但往外讀取元素就費(fèi)勁了,只有所有類的基類Object對象才能裝下。但這樣的話,元素的類型信息就全部丟失。
PECS原則
最后看一下什么是PECS(Producer Extends Consumer Super)原則,已經(jīng)很好理解了。
Producer Extends 生產(chǎn)者使用Extends來確定上界,往里面放東西來生產(chǎn)
Consumer Super 消費(fèi)者使用Super來確定下界,往外取東西來消費(fèi)
1、頻繁往外讀取內(nèi)容的,適合用上界Extends,即extends 可用于的返回類型限定,不能用于參數(shù)類型限定。
2、經(jīng)常往里插入的,適合用下界Super,super 可用于參數(shù)類型限定,不能用于返回類型限定。
3、帶有 super 超類型限定的通配符可以向泛型對象用寫入,帶有 extends 子類型限定的通配符可以向泛型對象讀取