Java 泛型中 <? extends T> 與 <? super T> 的區別

?T 是什么?

? 是通配符(wildcard,記住這個單詞,這樣在報錯時就知道說的是 ? 通配符),T 是類型變量。

  • 根據字面意思,<? extends T> 表示 任何繼承自類型 T 的類型<? super T> 表示 任何是類型 T 的超類的類型

? extend/super Xx 不能用于聲明處。
下面是用于表達式的正確例子:

Plate<? super Fruit> p = new Plate<Fruit>(new Fruit());
Plate<? extend Fruit> p1;

下面是用于聲明處的錯誤例子:

public class Fruit {}
public class Plate<? extends Fruit> {} // 報錯 Unexpected wildcard
  • T 則與問號不同,它只能用于聲明處。<T extends Fruit> 表示聲明一個類型變量 T,它是 Fruit 的一個具體子類不能寫 <T super Fruit>,因為這樣是沒有任何意義的。原因是所有泛型在編譯時都會被擦除,T 所代表的是一個 Fruit 的超類,但是具體是哪個類卻是在運行時被決定的,編譯器為了類型安全,只能做最大限度的包容,因此所有的 T 類型都會在編譯器變為 Object。所以,寫 <T super Fruit> 等同于寫 <Object>,因此不支持 <T super Xx>

上界和下界

泛型的關系-extends.png

下面代碼就是上界通配符(Upper Bounds Wildcards)

Plate<? extends Fruit> plate;

它的表意是,一個能放 Fruit 及其子類的盤子。


泛型的關系-super.jpg

下面代碼就是下界通配符(Lower Bounds Wildcards)

Plate<? super Fruit> plate

它的表意是,一個能放 Fruit 及其父類的盤子。

PECS 原則

以下原則,我們都先假設有這樣的類:

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> 不能往里存,只能往外取(被稱作協變

  • 往里存的意思就是,不能調用 <? extends T> 泛型類的以 T 為形參的方法。
  • 往外取的意思就是,可以調用 <? extends T> 泛型類的以 T 為返回值的方法。
Plate<? extends Fruit> p = new Plate<Apple>(new Apple()); // 實例 p 是協變的

// 不能被存入任何元素
p.set(new Fruit()); // exception
p.set(new Apple()); // exception

// 取出來的東西只能放在 Fruit 或它的基類里
Fruit fruit = p.get();
Object object = p.get();
Apple apple = p.get(); // exception
  • 注意以上代碼,只有在構造函數中可以對 T 類型的 item 進行賦值,通過 set 方法進行賦值是不行的。原因是編譯器通過 <? extends Fruit> 只知道 p 接受的是 Fruit 及其子類,但是具體是哪個不能確定。但是從寫法上,我們在構造時已經顯式地指明了 p 的泛型類型是 Apple,為什么編譯器還不知道呢?這個涉及到類型擦除。這是因為 JVM 在設計初期就沒有考慮過泛型,因此對于 JVM 編譯成的字節碼來說,也沒有泛型的概念,JVM 會使用一個占位符 CAP#1 來表示 p 接受一個 Fruit 或子類,這里就通過 CAP#1 把類型擦除了。所以無論想往 p 插入任何類型都不可以(因為你不能賦值一個 CAP#1 類型)。但是你可以從 p 中往外取 CAP#1,因為 CAP#1 代表的是 Fruit 及其子類,因此往外取時,類型為 Fruit 及其超類就總是安全的。

  • 但是,將 <? extends Fruit> 替換為一個具體的類型,set 方法就是生效的。這是因為編譯器已經知道 p 只會接受一個確定類型的水果 Apple

Plate<Apple> applePlate = new Plate<>(new Apple()); // 實例 applePlate 是不變的
applePlate.set(new Apple());
Apple apple = applePlate.get();
applePlate.set(new GreenApple());
  • 但是下面的寫法是不被接受的。因為泛型 T 是一個類型變量,它不能被用于表達式中,只能被用于聲明處。
Plate<T extends Fruit> p = new Plate<>(new Apple());
  • 如果寫 Plate<?>,則表示 Plate 中放的是任意類型,因此什么也存不進去,可以往外取 Object。因為 <?> 隱式地表示 <? extends Object>
Plate<?> anyPlate = new Plate<>(new Apple());
Object o = anyPlate.get();

其實存不進去的根本原因是因為,Java 類沒有一個共同的子類,但是卻有一個共同的父類 Object。所以永遠可以向上轉型取數據,卻不能向下轉型存數據。

2. 下界 <? super Fruit> 不影響往里存,但是往外取只能放在 Object(被稱作逆變

使用下界 <? super Fruit> 的意思是,Plate 中存放的是任意 Fruit 的基類,但是不確定是哪一個。因此往里放 Fruit 以及其子類一定是可以的(因為這些類一定是 <? super Fruit> 的子類)。但是往外取時就只能是 Object,因為編譯器不知道你存的是 Fruit 還是 MeatFruitMeat 就只有一個共同父類,那就是 Object。因此往外取 Object 一定是對的。

3. PECS(Producer Extends Consumer Super)

3.1 Producer Extends 你寫的類是主要作為生產者向外提供數據,那么就用 extends
3.2 Consumer Super 你寫的類是主要作為消費者,需要吃進數據,那么就用 super

4. 看一個復雜點的例子

class X< T extends List<? extends Number> > { 
  public void someMethod(T t) { 
    t.add(new Long(0L));    // error 
    Number n = t.remove(0); 
  } 
} 
class Test { 
  public static void main(String[] args) { 
     X<ArrayList< Long >>   x1 = new X<ArrayList<Long>>();  
     X<ArrayList< String >> x2 = new X<ArrayList<String>>(); // error 
  } 
}

X 聲明了一個類型參數 TT 是一個協變類型 List<? extends Number> 的子類。因此類 X 的方法 someMethod(T t) 可以接受一個類型為 T 的參數 t,但是不能往 t 里放任何數據。因為 <? extends Number> 限制了只能往外取。

這里的協變就是使用處協變。泛型聲明 T extends List<? extends Number> 并沒有直接限制 X 的類方法聲明的地方對 T 的使用,即 X 的類方法依然可以接受 T 的入參。但是在 Kotlin 中,我們就可以通過在 T 前面加上關鍵字 out 來使 T 在聲明處就產生協變

5. 自限定類型

class SelfBounded<T extends SelfBounded<T>>

這個寫法剛看起來寧人十分疑惑。聲明了一個類型 T,它是 SelfBounded<T> 的子類型,這似乎是一個無限循環。其實自限定類型是為子類提供了一個模板,泛型 T 在子類中只能表示子類這個具體類,而不能是其他類型。

interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
    void set(T args);
}

interface Setter extends SelfBoundSetter<Setter> {}

public class SelfBoundAndCovariantArguments {
    void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
        s1.set(s2);
        s1.set(sbs);  // 編譯錯誤
    }
}
5.1 自限定類型的具體例子

如果你去看 Enum 的源碼,就會發現,Enum 的聲明是自限定的類型。

public abstract class Enum<E extends Enum<E>> { 
  ... 
}

一個具體的枚舉類型 Color 在編譯時就被翻譯成了 Color extends Enum<Color>。如我們前面所說,自限定類型主要目的是為子類提供一種模板,我們來看看枚舉類 Enum 是怎么用這個模板的:

public abstract class Enum< E extends Enum<E>> implements Comparable< E >, Serializable { 
  private final String name; 
  public  final String name() { ... }
  private final int ordinal; 
  public  final int ordinal() { ... }

  protected Enum(String name, int ordinal) { ... }

  public String           toString() { ... } 
  public final boolean    equals(Object other) { ... } 
  public final int        hashCode() { ... } 
  protected final Object  clone() throws CloneNotSupportedException { ... } 
  public final int        compareTo( E o) { ... }

  public final Class< E > getDeclaringClass() { ... } 
  public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... } 
}

如果我們聲明一個具體的媒體類型 Color

enum Color {RED, BLUE, GREEN}

編譯器會將它翻譯成:

public final class Color extends Enum<Color> { 
  public static final Color[] values() { return (Color[])$VALUES.clone(); } 
  public static Color valueOf(String name) { ... }
  private Color(String s, int i) { super(s, i); }

  public static final Color RED; 
  public static final Color BLUE; 
  public static final Color GREEN;

  private static final Color $VALUES[];

  static { 
    RED = new Color("RED", 0); 
    BLUE = new Color("BLUE", 1); 
    GREEN = new Color("GREEN", 2); 
    $VALUES = (new Color[] { RED, BLUE, GREEN }); 
  } 
}

其中 ColorColor.compareTo 方法應該使用一個 Color 類型作為參數,自限定類型剛好就能滿足這樣的模板。

5.2 自限定類型的缺點

從名字上來看,自限定這個名字會讓我們誤以為像上面的 Color 類一樣,其中的泛型方法只能接受子類自己,比如 Color.compareTo 只能接受 Color 類型的參數。其實不然,其實自限定類型還可以接受它的兄弟類。

public interface SelfBound<E extends SelfBound<E>> {}

public class SelfBound1 implements SelfBound<SelfBound1> {}

public class SelfBound2 implements SelfBound<SelfBound1> {} // 注意這里接受的泛型實參為 SelfBound1

SelfBound2 接受了它的兄弟類型 SelfBound1,而不是像 SelfBound1 一樣接受的自己。如果以為 SelfBound2 的聲明只能接受它自己 SelfBound2,那就完全錯誤了。因此自限定這個叫法有點名不符其實。

6. 捕獲轉換

<?> 被稱作無界通配符,它有一個特殊的應用場景叫做捕獲轉換

public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }
    static void f2(Holder<?> holder) {
        // 做一些與 Holder 中與泛型類型無關的事,然后再調用 f1(holder),
        // 將 Holder 中的類型給捕獲
        f1(holder);
    }
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder<Integer>(1);
        f2(raw);
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic);
        Holder<?> wildcarded = new Holder<Double>(1.0);
        f2(wildcarded);
    }

    static class Holder<T> {
        private T t;
        public Holder(){}
        public Holder(T t){}

        public void set(T t){
            this.t = t;
        }

        public T get() {
            return t;
        }
    }
}
/* 程序輸出
Integer
Object
Double
*/

類型捕獲的地方當然可以寫已聲明的類型 T。但是這樣會降低程序的可讀性,因為在 f2() 中根本用不到泛型 T 相關的知識。而且,<?> 使 f2() 產生了協變,讓方法 f2() 中只能從 holder 中取數據,而不能寫數據。但是將 holder 傳給方法 f1() 后,由于方法 f1() 有聲明了不變的泛型 <T>,因此 holder 又被類型投影為了不變的類型。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容