Java泛型(二) 協(xié)變與逆變

定義

逆變與協(xié)變用來描述類型轉(zhuǎn)換(type transformation)后的繼承關(guān)系毅桃,其定義:如果A、B表示類型准夷,f(?)表示類型轉(zhuǎn)換钥飞,≤表示繼承關(guān)系(比如,A≤B表示A是由B派生出來的子類)
f(?)是逆變(contravariant)的衫嵌,當(dāng)A≤B時(shí)有f(B)≤f(A)成立读宙;
f(?)是協(xié)變(covariant)的,當(dāng)A≤B時(shí)有f(A)≤f(B)成立楔绞;
f(?)是不變(invariant)的结闸,當(dāng)A≤B時(shí)上述兩個(gè)式子均不成立,即f(A)與f(B)相互之間沒有繼承關(guān)系酒朵。

數(shù)組是協(xié)變的

Java中數(shù)組是協(xié)變的桦锄,可以向子類型的數(shù)組賦予基類型的數(shù)組引用,請(qǐng)看下面代碼蔫耽。

// CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple();
        fruit[1] = new Jonathan();
        try {
            fruit[0] = new Fruit();
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            fruit[0] = new Orange();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

main()中第一行創(chuàng)建了一個(gè)Apple數(shù)組结耀,并將其賦值給一個(gè)Fruit數(shù)組引用。編譯器允許你把Fruit放置到這個(gè)數(shù)組中匙铡,這對(duì)于編譯器是有意義的饼记,因?yàn)樗且粋€(gè)Fruit引用——它有什么理由不允許將Fruit對(duì)象或者任何從Fruit繼承出來的對(duì)象(例如Orange),放置到這個(gè)數(shù)組中呢慰枕?

可能有同學(xué)會(huì)疑惑具则,明明Fruit[]引用的是一個(gè)Apple數(shù)組,編譯器看不出來嗎具帮?還允許往里面放Fruit和Orange類的對(duì)象博肋。你要站在編譯器的角度看問題低斋,編譯器可沒有人這么聰明。現(xiàn)代編譯器大多采用的是上下文無關(guān)文法(編譯器:老子歸約一句是一句)匪凡,符號(hào)表中存儲(chǔ)的標(biāo)識(shí)符fruit是Fruit[]類型(不然咱還怎么多態(tài))膊畴,在以后的解析過程中編譯器看到fruit只會(huì)認(rèn)為是Fruit[]類型。

不過病游,盡管編譯器允許了這樣做唇跨,運(yùn)行時(shí)的數(shù)組機(jī)制知道它處理的是Apple[],因此會(huì)在向數(shù)組中放置異構(gòu)類型時(shí)拋出異常衬衬。程序的運(yùn)行結(jié)果如下买猖。

java.lang.ArrayStoreException: generics.Fruit
java.lang.ArrayStoreException: generics.Orange

泛型是不變的

當(dāng)我們使用泛型容器來替代數(shù)組時(shí),看看會(huì)發(fā)生什么滋尉。

public class NonCovariantGenerics {
    List<Fruit> flist = new ArrayList<Apple>(); // 編譯錯(cuò)誤
}

直接在編譯時(shí)報(bào)錯(cuò)了玉控。與數(shù)組不同,泛型沒有內(nèi)建的協(xié)變類型狮惜。這是因?yàn)閿?shù)組在語言中是完全定義的高诺,因此內(nèi)建了編譯期和運(yùn)行時(shí)的檢查,但是在使用泛型時(shí)碾篡,類型信息在編譯期被擦除了(如果你不知道什么是擦除虱而,可以去看這篇文章補(bǔ)補(bǔ)課類型擦除),運(yùn)行時(shí)也就無從檢查开泽。因此薛窥,泛型將這種錯(cuò)誤檢測(cè)移入到編譯期。

通配符引入?yún)f(xié)變眼姐、逆變

協(xié)變

Java泛型是不變的诅迷,可有時(shí)需要實(shí)現(xiàn)協(xié)變,在兩個(gè)類型之間建立某種類型的向上轉(zhuǎn)型關(guān)系众旗,怎么辦呢罢杉?這時(shí),通配符派上了用場(chǎng)贡歧。

public class GenericsAndCovariance {
    public static void main(String[] args) {
        List<? extends Fruit> flist = new ArrayList<Apple>();
        flist.add(new Apple());  // 編譯錯(cuò)誤
        flist.add(new Fruit());  // 編譯錯(cuò)誤
        flist.add(new Object());  // 編譯錯(cuò)誤
    }
}

現(xiàn)在flist的類型是<? extends Fruit>滩租,extends指出了泛型的上界為Fruit,<? extends T>稱為子類通配符利朵,意味著某個(gè)繼承自Fruit的具體類型律想。使用通配符可以將ArrayList<Apple>向上轉(zhuǎn)型了,也就實(shí)現(xiàn)了協(xié)變绍弟。

然而技即,事情變得怪異了,觀察上面代碼樟遣,你再也不能往容器里放入任何東西而叼,甚至連Apple都不行身笤。

原因在于,List<? extends Fruit>也可以合法的指向一個(gè)List<Orange>葵陵,顯然往里面放Apple液荸、Fruit、Object都是非法的脱篙。編譯器不知道List<? extends Fruit>所持有的具體類型是什么娇钱,所以一旦執(zhí)行這種類型的向上轉(zhuǎn)型,你就將丟失掉向其中傳遞任何對(duì)象的能力绊困。

類比數(shù)組文搂,盡管你可以把Apple[]向上轉(zhuǎn)型成Fruit[],然而往里面添加Fruit和Orange等對(duì)象都是非法的考抄,會(huì)在運(yùn)行時(shí)拋出ArrayStoreException異常。泛型把類型檢查移到了編譯期蔗彤,協(xié)變過程丟掉了類型信息川梅,編譯器拒絕所有不安全的操作。

逆變

我們還可以走另外一條路然遏,就是逆變贫途。

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        apples.add(new Fruit());  // 編譯錯(cuò)誤
    }
}

我們重用了關(guān)鍵字super指出泛型的下界為Apple,<待侵? super T>稱為超類通配符丢早,代表一個(gè)具體類型,而這個(gè)類型是Apple的超類秧倾。這樣編譯器就知道向其中添加Apple或Apple的子類型(例如Jonathan)是安全的了怨酝。但是,既然Apple是下界那先,那么可以知道向這樣的List中添加Fruit是不安全的农猬。

PECS

上面說的可能有點(diǎn)繞,那么總結(jié)下:什么使用extends售淡,什么時(shí)候使用super斤葱。《Effective Java》給出精煉的描述:producer-extends, consumer-super(PECS)揖闸。

說直白點(diǎn)就是揍堕,從數(shù)據(jù)流來看,extends是限制數(shù)據(jù)來源的(生產(chǎn)者)汤纸,而super是限制數(shù)據(jù)流入的(消費(fèi)者)衩茸。例如上面SuperTypeWildcards類里,使用<? super Apple>就是限制add方法傳入的類型必須是Apple及其子類型贮泞。

仿照上面的代碼递瑰,我寫了個(gè)ExtendTypeWildcards類祟牲,可以看出<? extends Apple>限制了get方法返回的類型必須是Apple及其父類型。

public class ExtendTypeWildcards {
    static void readFrom(List<? extends Apple> apples) {
        Apple apple = apples.get(0);
        Jonathan jonathan = apples.get(0);  // 編譯錯(cuò)誤
        Fruit fruit = apples.get(0);
    }
}

例子

框架和庫(kù)代碼中到處都是PECS抖部,下面我們來看一些具體的例子说贝,加深理解。

  • java.util.Collections的copy方法
// Collections.java
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();
            di.set(si.next());
        }
    }
}

copy方法限制了拷貝源src必須是T或者是它的子類慎颗,而拷貝目的地dest必須是T或者是它的父類乡恕,這樣就保證了類型的合法性。

  • Rxjava的變換

這里我們貼出一小段Rxjava2.0中map函數(shù)的源碼俯萎。

// Observable.java
public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
    ObjectHelper.requireNonNull(mapper, "mapper is null");
    return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
}

Function函數(shù)將<? super T>類型轉(zhuǎn)變?yōu)?lt;? extends R>類型(類似于代理模式的攔截器)傲宜,可以看出extends和super分別限制輸入和輸出,它們可以是不同類型夫啊。

自限定的類型

理解自限定

Java泛型中函卒,有一個(gè)好像是經(jīng)常性出現(xiàn)的慣用法,它相當(dāng)令人費(fèi)解撇眯。

class SelfBounded<T extends SelfBounded<T>> { // ...

SelfBounded類接受泛型參數(shù)T报嵌,而T由一個(gè)邊界類限定,這個(gè)邊界就是擁有T作為其參數(shù)的SelfBounded熊榛,看起來是一種無限循環(huán)锚国。

先給出結(jié)論:這種語法定義了一個(gè)基類,這個(gè)基類能夠使用子類作為其參數(shù)玄坦、返回類型血筑、作用域。為了理解這個(gè)含義煎楣,我們從一個(gè)簡(jiǎn)單的版本入手豺总。

// BasicHolder.java
public class BasicHolder<T> {
    T element;
    void set(T arg) { element = arg; }
    T get() { return element; }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

// CRGWithBasicHolder.java
class Subtype extends BasicHolder<Subtype> {}

public class CRGWithBasicHolder {
    public static void main(String[] args) {
        Subtype st1 = new Subtype(), st2 = new Subtype();
        st1.set(st2);
        Subtype st3 = st1.get();
        st1.f();
    }
}  
/* 程序輸出
Subtype
*/

新類Subtype接受的參數(shù)和返回的值具有Subtype類型而不僅僅是基類BasicHolder類型。所以自限定類型的本質(zhì)就是:基類用子類代替其參數(shù)择懂。這意味著泛型基類變成了一種其所有子類的公共功能模版园欣,但是在所產(chǎn)生的類中將使用確切類型而不是基類型。因此休蟹,Subtype中沸枯,傳遞給set()的參數(shù)和從get() 返回的類型都確切是Subtype。

自限定與協(xié)變

自限定類型的價(jià)值在于它們可以產(chǎn)生協(xié)變參數(shù)類型——方法參數(shù)類型會(huì)隨子類而變化赂弓。其實(shí)自限定還可以產(chǎn)生協(xié)變返回類型绑榴,但是這并不重要,因?yàn)镴DK1.5引入了協(xié)變返回類型盈魁。

協(xié)變返回類型

下面這段代碼子類接口把基類接口的方法重寫了翔怎,返回更確切的類型。

// CovariantReturnTypes.java
class Base {}
class Derived extends Base {}

interface OrdinaryGetter { 
    Base get();
}

interface DerivedGetter extends OrdinaryGetter {
    Derived get();
}

public class CovariantReturnTypes {
    void test(DerivedGetter d) {
        Derived d2 = d.get();
    }
}

繼承自定義類型基類的子類將產(chǎn)生確切的子類型作為其返回值,就像上面的get()一樣赤套。

// GenericsAndReturnTypes.java
interface GenericsGetter<T extends GenericsGetter<T>> {
    T get();
}

interface Getter extends GenericsGetter<Getter> {}

public class GenericsAndReturnTypes {
    void test(Getter g) {
        Getter result = g.get();
        GenericsGetter genericsGetter = g.get();
    }
}
協(xié)變參數(shù)類型

在非泛型代碼中飘痛,參數(shù)類型不能隨子類型發(fā)生變化。方法只能重載不能重寫容握。見下面代碼示例宣脉。

// OrdinaryArguments.java
class OrdinarySetter {
    void set(Base base) {
        System.out.println("OrdinarySetter.set(Base)");
    }
}

class DerivedSetter extends OrdinarySetter {
    void set(Derived derived) {
        System.out.println("DerivedSetter.set(Derived)");
    }
}

public class OrdinaryArguments {
    public static void main(String[] args) {
        Base base = new Base();
        Derived derived = new Derived();
        DerivedSetter ds = new DerivedSetter();
        ds.set(derived);
        ds.set(base);
    }
}
/* 程序輸出
DerivedSetter.set(Derived)
OrdinarySetter.set(Base)
*/

但是,在使用自限定類型時(shí)剔氏,在子類中只有一個(gè)方法塑猖,并且這個(gè)方法接受子類型而不是基類型為參數(shù)。

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);  // 編譯錯(cuò)誤
    }
}

捕獲轉(zhuǎn)換

<?>被稱為無界通配符谈跛,無界通配符有什么作用這里不再詳細(xì)說明了羊苟,理解了前面東西的同學(xué)應(yīng)該能推斷出來。無界通配符還有一個(gè)特殊的作用感憾,如果向一個(gè)使用<?>的方法傳遞原生類型蜡励,那么對(duì)編譯期來說,可能會(huì)推斷出實(shí)際的參數(shù)類型阻桅,使得這個(gè)方法可以回轉(zhuǎn)并調(diào)用另一個(gè)使用這個(gè)確切類型的方法凉倚。這種技術(shù)被稱為捕獲轉(zhuǎn)換。下面代碼演示了這種技術(shù)鳍刷。

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) {
        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);
    }
}
/* 程序輸出
Integer
Object
Double
*/

捕獲轉(zhuǎn)換只有在這樣的情況下可以工作:即在方法內(nèi)部占遥,你需要使用確切的類型俯抖。注意输瓜,不能從f2()中返回T,因?yàn)門對(duì)于f2()來說是未知的芬萍。捕獲轉(zhuǎn)換十分有趣尤揣,但是非常受限。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末柬祠,一起剝皮案震驚了整個(gè)濱河市北戏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌漫蛔,老刑警劉巖嗜愈,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異莽龟,居然都是意外死亡蠕嫁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門毯盈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剃毒,“玉大人,你說我怎么就攤上這事∽阜В” “怎么了益缠?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)基公。 經(jīng)常有香客問我幅慌,道長(zhǎng),這世上最難降的妖魔是什么酌媒? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任欠痴,我火速辦了婚禮,結(jié)果婚禮上秒咨,老公的妹妹穿的比我還像新娘喇辽。我一直安慰自己,他們只是感情好雨席,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布菩咨。 她就那樣靜靜地躺著,像睡著了一般陡厘。 火紅的嫁衣襯著肌膚如雪抽米。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天糙置,我揣著相機(jī)與錄音云茸,去河邊找鬼。 笑死谤饭,一個(gè)胖子當(dāng)著我的面吹牛标捺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播揉抵,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼亡容,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了冤今?” 一聲冷哼從身側(cè)響起闺兢,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎戏罢,沒想到半個(gè)月后屋谭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡龟糕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年桐磁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翩蘸。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡所意,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扶踊,我是刑警寧澤泄鹏,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站秧耗,受9級(jí)特大地震影響备籽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜分井,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一车猬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尺锚,春花似錦珠闰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伐厌,卻和暖如春承绸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挣轨。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工军熏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人卷扮。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓荡澎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親画饥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子衔瓮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分,“神秘”的通配符葫慎,讓我看了幾遍《Java...
    珞澤珈群閱讀 7,768評(píng)論 12 51
  • 本文大量參考Thinking in java(解析衔彻,填充)。 定義:多態(tài)算是一種泛化機(jī)制偷办,解決了一部分可以應(yīng)用于多...
    谷歌清潔工閱讀 456評(píng)論 0 2
  • java泛型解決容器艰额,不確定類型問題,多個(gè)返回值椒涯,避免類型轉(zhuǎn)換柄沮。 類泛型 類泛型定義的時(shí)候需要在類型后增加尖括號(hào),...
    wangsye閱讀 451評(píng)論 0 0
  • 第8章 泛型 通常情況的類和函數(shù),我們只需要使用具體的類型即可:要么是基本類型祖搓,要么是自定義的類狱意。但是在集合類的場(chǎng)...
    光劍書架上的書閱讀 2,143評(píng)論 6 10
  • 兒子的好朋友陽陽拿到簽證,當(dāng)天晚上的飛機(jī)就準(zhǔn)備飛去英國(guó)了拯欧。出門之前详囤,兩個(gè)當(dāng)媽的不約而同的說,來一起拍個(gè)照做紀(jì)念吧镐作,...
    漢堡帝國(guó)閱讀 209評(píng)論 0 0