Java泛型中的PECS原則,extends 和 super 的區別

【Java】泛型中 extends 和 super 的區別?

 

<? extends T><? super T>是Java泛型中的“通配符(Wildcards)”“邊界(Bounds)”的概念。

  • <? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”
  • <? super T>:是指 “下界通配符(Lower Bounds Wildcards)”

爲什麼要用通配符和邊界?

使用泛型的過程中,經常出現一種很彆扭的情況。比如按照題主的例子,我們有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的引用傳遞給Plate。

爲了讓泛型用起來更舒服,Sun的大腦袋們就想出了<? 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());

如果把Fruit和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。

 

PECS原則

PECS原則的全拼是"Producer Extends Consumer Super"。
當需要頻繁取值,而不需要寫值則使用" ? extends T "作爲數據結構泛型。相反,當需要頻繁寫值,而不需要取值則使用" ? super T "作爲數據結構泛型。

使用示例

創建Apple,Fruit兩個類,其中Apple是Fruit的子類,寫如下測試代碼:

 

public class PECS {

    ArrayList<? extends Fruit> exdentFurit;
    ArrayList<? super Fruit> superFurit;
    Apple apple = new Apple();

    private void test() {
        Fruit a1 = exdentFurit.get(0);
        Fruit a2 = superFurit.get(0); //Err1

        exdentFurit.add(apple); //Err2
        superFurit.add(apple);
    }
}

其中Err1和Err2行處報錯,因爲這些操作並不符合PECS原則,逐一分析:

  • Err1
    對於使用" ? super T "規定泛型的數據結構,我們只知道其存儲的值是T的父類,若以T爲數據類型取值時 T t = ?,等於將父類(?)當做子類(T)使用,這顯然是不合理的,父類缺少子類中的一些信息,因此編譯器直接禁止在使用" ? super T "泛型的數據結構中進行取值,只能進行寫值,正是開頭所說的CS原則。

  • Err2
    使用" ? extends T "規定泛型的數據結構,我們知道其存儲的值是T的子類,T可以有多個不同表現的子類(多態),因此當我們進行寫值時,我們並不知道其中存儲的到底是哪個子類,不同子類是不同的類型,因此寫值操作必然會出現問題,所以編譯器接禁止在使用" ? extends T "泛型的數據結構中進行寫,只能進行取值,正是開頭所說的PE原則。

總結

PECS原則,就是當時用extends做泛型時,該數據結構作爲Producer對外提供值,只能進行取值而不能寫值。
當時用super做泛型時,該數據結構作爲Producer對外提供值,只能進行取值而不能寫值。


 

 

參考鏈接:https://itimetraveler.github.io/2016/12/27/%E3%80%90Java%E3%80%91%E6%B3%9B%E5%9E%8B%E4%B8%AD%20extends%20%E5%92%8C%20super%20%E7%9A%84%E5%8C%BA%E5%88%AB%EF%BC%9F/

https://www.jianshu.com/p/6db1362c50b1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章