<? extends T> 及<? super T> 重溫
本文針對泛型中<? extends T> 及<? super T>的主要區別及使用用途進行討論.
作者盡量描述其原理,分析疑點. 希望對復習Java泛型使用,項目架構及日常使用有幫助
也是作者作為學習的加強記憶
編碼例子背景
設定有一盤子(容器),可以存放物品,同時有食物,水果等可以存放在容器里面.
import com.google.common.collect.Lists; //引入guava的Lists工具方便生產List實例
class Food {
/** name*/
protected String name = "Food";
/** 打印食物名稱*/
public void echo() {
System.out.println(name);
}
}
class Fruit extends Food {}
class Apple extends Fruit {
public Apple() {
name = "Apple";
}
}
class Pear extends Fruit {
public Pear() {
name = "Pear";
}
}
class Plate<T> {
private T item;
public Plate() {}
public Plate(T item) {
this.item = item;
}
public void set(T t) {
item = t;
}
public T get() {
return item;
}
/** 模仿處理泛型T實例*/
public void methodWithT(T t) {
//簡單打印實例
System.out.println("methodWithT,T's is : " + t);
}
}
引出問題背景
現在,有兩個容器,一個放水果(沒說明放哪種),一個特指放蘋果
Fruit fruit = new Fruit();
Apple apple = new Apple();
Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();
現在對着兩個容器進行一些常規操作,賦值/調用API
Fruit fruit = new Fruit();
Apple apple = new Apple();
// 父類容器,放置子類
// 此處是多態的提供的API
// apple is a fruit
fruitPlate.set(apple);
fruitPlate.methodWithT(apple);
// 父類容器引用指向(被賦值)子類容器
// 父類容器與子類容器的關系,此處關注的是容器Plate這個類!!!
// 並沒有像父子類中的繼承關系(多態)!!!
// apple's plate is not a fruit's plate
// ERROR
// fruitPlate = applePlate; // 裝水果的盤子無法指向裝蘋果
// ERROR
// applePlate = fruitPlate;// 明顯錯誤,子類容器指向父類容器
初學Java的讀者看到此處,心中必定會有疑問,難道裝蘋果的盤子不是裝水果的盤子?很遺憾,使用
來修飾泛型,編譯器確實是這么認為的
所以,以上測試代碼中,父類容器引用指向(被賦值)子類容器,編譯報錯,是跟多態上的認識是相反的結果
為了解決此類容器間'繼承多態'問題,實現父子容器泛類引用指向,於是JDK提供了<? extends T>
及<? supper T>
<? extends T>
<? extends T>
: 上界通配符(Upper Bounds Wildcards),表示上界是T,用此修飾符修飾的泛類容器,可以指向本類及子類容器
示例如下:
/**
* <? extends T> 上界通配符(Upper Bounds Wildcards)
* <p>
* Plate <? extends Fruit> extendsFruitPlate 可以被 Plate <Fruit> 及 Plate <Apple> 賦值
*/
@Test
public void extendsTest() {
Fruit fruit = new Fruit();
Apple apple = new Apple();
Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();
Plate<? extends Fruit> extendsFruitPlate ;
// SUCCESS
// Plate<? extends Fruit>引用可以指向Plate<Fruit>以及Plate<Apple>
extendsFruitPlate = applePlate;
extendsFruitPlate = fruitPlate;
}
<? supper T>
<? supper T>
: 下界通配符(Lower Bounds Wildcards),表示下界是T,用此修飾符修飾的泛類容器,可以指向本類及父類容器
示例如下:
/**
* <? supper T> 下界通配符(Lower Bounds Wildcards)
* Plate <? supper Fruit> superFruitPlate 可以被 Plate <Fruit> 及 Plate <Object> 賦值
*/
@Test
public void supperTest() {
Fruit fruit = new Fruit();
Apple apple = new Apple();
Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();
Plate<Object> objectPlate = new Plate<>();
Plate<? super Fruit> superFruitPlate = new Plate<>();
// SUCCESS
// Plate<? super Fruit>引用可以指向Plate<Fruit>以及Plate<Object>
superFruitPlate = fruitPlate;
superFruitPlate = objectPlate;
// ERROR
// superFruitPlate = applePlate; // <? supper Fruit>修飾的容器不能被子類容器賦值
}
以上例子說明,
<? extends T>
及<? super T>
可以解決父子類泛型之間的引用問題,但同時,在使用被修飾的泛型容器的相關API,也做出了相關的調整.
可以說,這樣的便利,是建立在一定的規則上,和付出一些代價的.可以肯定地是,這些限制規則,是符合多態的規則.理解后對我們工作編程上對編譯器安全類型限制理解有一定的幫助
以下說明是相關調整的表現及這樣的限定原因
<? extends T> 修飾的泛型其接口特點
<? extends T> 具體表現:
返回父類T的接口
:調用這類型的接口返回的實例類型是父類T(這句結論說跟沒說一樣,理解起來特別容易.)接收父類T的接口
:這類型的接口均不可以再被調用
形象點,就如同網上絕大多數描述的一樣:
不能往里存,只能往外取
注:存與取,僅僅是一種表現形式,確切來說我認為是返回T接口(方法)及接收T接口(方法)更為准確
不能往里存含義是接收T接口不能再調用,否則編譯異常
只能往外取含義是返回T接口可以正常使用,返回的實例類型就是T
代碼表現如下:
/**
* <? extends T> 上界通配符(Upper Bounds Wildcards)
* 注意點: 只能獲取,不能存放
*/
@Test
public void extendsAttentionTest() {
Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear();
Plate<? extends Fruit> extendsFruitPlate;
extendsFruitPlate = new Plate<Fruit>(fruit);
extendsFruitPlate = new Plate<Apple>(apple);
extendsFruitPlate = new Plate<Pear>(pear);
// 以下ERROR代碼,嘗試調用接收泛型T的方法,均編譯不過
// ERROR:
// extendsFruitPlate.set(fruit);
// extendsFruitPlate.set(apple);
// extendsFruitPlate.set(pear);
// extendsFruitPlate.set(new Object());
// extendsFruitPlate.methodWithT(fruit);
// extendsFruitPlate.methodWithT(apple);
// extendsFruitPlate.methodWithT(pear);
// extendsFruitPlate.methodWithT(new Object());
// 以上注釋的錯誤代碼,初學者也會有疑問,
// 為什么<Plate<? extends Fruit> extendsFruitPlate;這樣裝水果子類的盤子,
// 現在什么東西都不能放了?那我還要這個容器有什么用?這是不是跟思維慣性認知有點偏差?
// SUCCESS
// 返回的是泛型T即Fruit,具體的實例是Pear類型
Fruit getFruit = extendsFruitPlate.get();
getFruit.echo();// 輸出Pear
// 接口測試
class ExtendsClass {
public void extendsMethod(List<? extends Fruit> extendsList) {
// ERROR:
// 出錯原理同上,不能調用接收泛型T的方法
// extendsList.add(fruit);
// extendsList.add(apple);
// extendsList.add(new Object());
// SUCCESS
// 獲取是父類,可以強轉為子類再使用
Fruit getFruitByList = extendsList.get(0);
getFruitByList.echo();
}
}
List<Fruit> fruits = Lists.newArrayList(fruit);
List<Apple> apples = Lists.newArrayList(apple);
ExtendsClass extendsClass = new ExtendsClass();
// List<? extends Fruit> extendsList可以接收List<Fruit>/List<Apple>
extendsClass.extendsMethod(fruits);
extendsClass.extendsMethod(apples);
}
<? extends T> 相關限制的原因
Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear();
Plate<? extends Fruit> extendsFruitPlate;
extendsFruitPlate = new Plate<Fruit>(fruit);
extendsFruitPlate = new Plate<Apple>(apple);
extendsFruitPlate = new Plate<Pear>(pear);
編譯器的理解: Plate<? extends Fruit> extendsFruitPlate 這個盤子 :
- 你不能保證讀取到 Apple ,因為 extendsFruitPlate 可能指向的是
Plate<Fruit>
- 你不能保證讀取到 Pear ,因為 extendsFruitPlate 可能指向的是
Plate<Apple>
- 你可以讀取到 Fruit ,因為 extendsFruitPlate 要么包含 Fruit 實例,要么包含 Fruit 的子類實例.
- 你不能插入一個 Fruit 元素,因為 extendsFruitPlate 可能指向
Plate<Apple>
或Plate<Pear>
- 你不能插入一個 Apple 元素,因為 extendsFruitPlate 可能指向
Plate<Fruit>
或Plate<Pear>
- 你不能插入一個 Pear 元素,因為 extendsFruitPlate 可能指向
Plate<Fruit>
或Plate<Apple>
你不能往Plate<? extends T>
中插入任何類型的對象,因為你不能保證列表實際指向的類型是什么,
你並不能保證列表中實際存儲什么類型的對象,唯一可以保證的是,你可以從中讀取到T
或者T
的子類.
所以,
- 可以調用接收泛型T的方法的接口 extendsFruitPlate.get() 獲取 Fruit 的實例
- 卻不能調用接收泛型T接口 extendsFruitPlate.set(fruit)添加任何元素
- 也不能調用接收泛型T接口 extendsFruitPlate.methodWithT(fruit)處理任何對象
當
extendsFruitPlate指向 Plate<Pear>
時候, 調用extendsFruitPlate.set(fruit)
和extendsFruitPlate.methodWithT(fruit)
調用等價於
Apple apple = new Apple();
Plate<Pear> pearPlate=new Plate<Pear>();
// 以下明顯類型不相同,且不符合多態,導致類型轉換異常
pearPlate.set(apple);
pearPlate.methodWithT(apple);
可以說,<? extends T> 修飾的泛型容器可以指向子類容器,是建立在
不能調用接收泛型T的方法
條件上的,否則運行時將可能產生類型轉換異常
編譯器總是往最安全的情況考慮,盡量把可能存在的問題在編譯期間就反映出來.所以編譯器在處理<? extends T> 修飾的泛型容器時候,干脆讓這個容器得接收泛型T方法不能再調用了
<? super T> 修飾的泛型其接口特點
<? super T> 具體表現
返回父類T的接口
:調用這類型的接口返回的類型是Object類型接收父類T的接口
:調用這類型的只能傳入父類T及T的子類實例
形象點,就如同網上絕大多數描述的一樣:
不影響往里存,但是往外取只能放在 Object
注:存與取,僅僅是一種表現形式,確切來說我認為是返回T接口(方法)及接收T接口(方法)更為准確
不影響往里存含義:調用這類型的只能傳入父類T及T的子類實例
往外取只能放在 Object含義:調用這類型的接口返回的類型是Object類型,可以通過強轉手段轉化為子類
代碼表現如下:
/**
* <? supper T> 下界通配符(Lower Bounds Wildcards)
* 注意點: 取出是Object,存放是父類或子類
*/
@Test
public void superAttentionTest() {
Object object = new Object();
Food food = new Food();
Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear();
Plate<? super Fruit> superFruitPlate;
// 可以被 Plate<Object> , Plate<Food> ,Plate<Fruit> Plate<父類容器> 賦值
superFruitPlate = new Plate<Object>(object);
superFruitPlate = new Plate<Food>();
superFruitPlate = new Plate<Fruit>();
// SUCCESS
superFruitPlate.set(fruit);
superFruitPlate.set(apple);
superFruitPlate.set(pear);
superFruitPlate.methodWithT(fruit);
superFruitPlate.methodWithT(apple);
superFruitPlate.methodWithT(pear);
// ERROR:接收父類T的接口,當[不是 T或T的子類時],則編譯異常
// superFruitPlate.set(food);
// superFruitPlate.set(object);
// superFruitPlate.methodWithT(food);
// superFruitPlate.methodWithT(object);
// 以上注釋的錯誤代碼,初學者也會有疑問,
// 為什么<Plate<? super Fruit> superFruitPlate;這樣可以指向水果父類的盤子,
// 現在卻只能放子類?這是不是跟思維慣性認知有點偏差?
// 只能獲取到Object對象,需要進行強轉才可以進行調用相關API
Object supperFruitPlateGet = superFruitPlate.get();
if (supperFruitPlateGet instanceof Fruit) {
// 為什么需要 instanceof ?
// superFruitPlate可以指向Plate<Food>,獲取出來實際是Food實例
Fruit convertFruit = (Fruit) supperFruitPlateGet;
convertFruit.echo();
}
// 接口測試
class SuperClass {
public void supperMethod(List<? super Fruit> superList) {
superList.add(fruit);
superList.add(apple);
superList.add(pear);
// ERROR:原因如上,調用method(T t)時候,當t[不是 T或T的子類時],則編譯異常
// superList.add(object);
// superList.add(food);
Object innerObject = superList.get(0);
if (innerObject instanceof Fruit) {
// 為什么需要 instanceof ?
// 像這樣:superFruitPlate 可以指向List<Object> objects,獲取出來是Object
Fruit innerConvertFruit = (Fruit) innerObject;
innerConvertFruit.echo();
} else {
System.out.println("supperMethod:非Fruit,插入非本類或非子類:" + innerObject);
}
}
}
List<Object> objects = new ArrayList<>(Arrays.asList(object));
List<Food> foods = new ArrayList<>(Arrays.asList(food));
List<Fruit> fruits = new ArrayList<>(Arrays.asList(fruit));
List<Apple> apples = new ArrayList<>(Arrays.asList(apple));
List<Pear> pears = new ArrayList<>(Arrays.asList(pear));
SuperClass superClass = new SuperClass();
superClass.supperMethod(objects);
superClass.supperMethod(foods);
superClass.supperMethod(fruits);
// ERROR 原因同上,非Fruit及Fruit父類容器則編譯不通過
// superClass.supperMethod(apples);
// superClass.supperMethod(pears);
}
<? super T> 相關限制的原因
Plate<? super Fruit> superFruitPlate;
// 可以被 Plate<Object> , Plate<Food> ,Plate<Fruit> Plate<父類容器> 賦值
superFruitPlate = new Plate<Object>(object);
superFruitPlate = new Plate<Food>();
superFruitPlate = new Plate<Fruit>();
編譯器的理解: Plate<? super Fruit> superFruitPlate 這個盤子 ,
- superFruitPlate 不能確保讀取到 Fruit ,因為 superFruitPlate 可能指向
Plate<Object>
或Plate<Food>
- superFruitPlate 不能確保讀取到 Food ,因為 superFruitPlate 可能指向
Plate<Object>
所以,取出來必須是Object
,最后需要則調用強轉
- 你不能插入一個 Food 元素,因為 superFruitPlate 可能指向
Plate<Fruit>
- 你不能插入一個 Object 元素,因為 superFruitPlate 可能指向
Plate<Fruit>
或Plate<Food>
- 你可以插入一個 Fruit/Apple/Pear 類型的元素,因為 Fruit/Apple/Pear 類都是 Fruit,Food,Object的本類或子類
所以,從 superFruitPlate 獲取到的都是Object對象,superFruitPlate 插入的都是Fruit的本類或本身
故有如下結論:
- superFruitPlate 調用返回父類T的接口,獲取到的都是 Object 對象;
- superFruitPlate 調用接收父類T的接口,只能傳入父類T及T的子類實例
當 superFruitPlate 指向 Plate
,
調用 superFruitPlate.set(food) 和
superFruitPlate.methodWithT(food)
調用等價於:
Plate<Fruit> pearPlate=new Plate<Fruit>();
Food food=new Food();
// 以下明顯類型不相同,且不符合多態,導致類型轉換異常
fruitPlate.set(food);
fruitPlate.methodWithT(food);
可以說,<? super T> 修飾的泛型容器可以指向父類容器,是建立在調用接收T的接口,只能傳入T及T的子類實例條件上的,否則運行時將可能產生類型轉換異常
編譯器總是往最安全的情況考慮,盡量把可能存在的問題在編譯期間就反映出來.所以編譯器在處理<? super T> 修飾的泛型容器時候,干脆讓這個容器得接收泛型T方法只能傳入T及T的子類
PECS (Producter Extends, Consumer Super) 原則
以上原則來源兩者主要區別,合理使用其優點,有種去其糟粕,取其精華的意思
<? extends T>
: 可以獲取父類,向外提供內容,為生產者角色
<? super T>
: 可以調用接收/處理父類及子類的接口,為消費者角色
舉一個JDK 中例子:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
// si 來源List<? super T> dest,向外提供內容,生產者
// di 來源List<? extends T> src,接收/處理類型T的本類或子類,消費者
di.set(si.next());
}
}
}
小結
至此,本文完結了.重溫的時候,發現很多之前想當然的結論,並沒有細細研究其中原因.現在理解起來是不會再次忘記的了.要記住的是需要理解編譯器是怎么認為的而不是怎么從修飾符去片面理解
通篇顯得有點啰嗦,至少作者認為把重點及原因說清楚了.以上如有不當之處敬請指正.
參考
本文例子來源主要有二,最精髓的地方是StackOverflow的鏈接的第一第二個回答
博客園 : RainDream : <? extends T>和<? super T>
Stackoverflow:Difference between<? super T>and<? extends T>in Java