Java泛型的協(xié)變、逆變和不變

背景

平時(shí)在看一些開源框架源碼時(shí)總發(fā)現(xiàn)他們會(huì)或多或少的用到泛型來定義數(shù)據(jù)類型瘫镇。這可以理解鼎兽,畢竟牛逼的開源框架大都是為了解決一類普遍問題而存在的;但看不懂的是铣除,有時(shí)參數(shù)或者返回值會(huì)出現(xiàn)諸如<? extends T><? super T>這樣帶通配符的泛型參數(shù),這種通配符的泛型是什么意思鹦付?如果直接用指定的T會(huì)有什么問題尚粘?這樣做是為了解決什么問題?這是我的疑惑敲长。咨詢公司完全做Java開發(fā)的服務(wù)端同學(xué)后郎嫁,也未能完全解惑。于是查找資料后引出今天的主題----Java泛型的協(xié)變(<? extends T>)祈噪、逆變(<? super T>)和不變(T)泽铛。

舉例

  1. RxJava框架
    在定義一個(gè)Observable后,最終會(huì)通過subscribe()來訂閱一個(gè)Observer, 而subscribe()參數(shù)的定義就使用了逆變(<? super T>)辑鲤,如下所示:
/**
 * 參數(shù)observer用到了逆變<? super T>
 */
public final void subscribe(Observer<? super T> observer) {
    try {
        //....
        subscribeActual(observer); //實(shí)際發(fā)起的訂閱
        //...方法
    } catch (Throwable e) {
        //...
    }
}

map操作符的參數(shù)Function泛型分別使用協(xié)變和逆變實(shí)現(xiàn)盔腔,如下所示:

/**
 * 參數(shù)mapper是一個(gè)Function接口類型,第一個(gè)參數(shù)用到了逆變<? super T>,
 * 第二個(gè)參數(shù)用到了協(xié)變<? extends R>
 */
public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
    //...
    return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
}
  1. java集合框架Collections的工具方法copy()分別使用協(xié)變和逆變定義了兩個(gè)集合的類型弛随,如下所示:
/**
 * 目的列表使用的是逆變瓢喉,源列表使用的是協(xié)變
 */
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    //...
    for (int i=0; i<srcSize; i++) {
        dest.set(i, src.get(i));
    }
    //...
}

3.java8中Stream的超級接口collect(),其參數(shù)定義使用了不變和逆變舀透,如下所示:

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

面對上面這些源碼的定義栓票,不禁讓人產(chǎn)生疑惑!c倒弧走贪!

概念

假設(shè)Orange類是Fruit類的子類,以集合類List<T>為例:

  1. 型變:用來描述類型轉(zhuǎn)換后的繼承關(guān)系(即協(xié)變惑芭、逆變和不變的統(tǒng)稱)坠狡。比如:List<Orange>List<Fruit>的子類型嗎?答案是No强衡,兩者并沒有關(guān)系员萍,并不能相互讀寫數(shù)據(jù)绪商。因此,型變是處理如List<Orange>(List<? extends Orange>)和List<Fruit>子類型關(guān)系的一種表達(dá)方式。
  2. 協(xié)變(covariance):滿足條件諸如List<Orange>List<? extends Fruit>的子類型時(shí)壁肋,稱為協(xié)變。
  3. 逆變(covariance):滿足條件List<Fruit>List<? super Orange>的子類型時(shí)驱显,稱為逆變拆祈。
  4. 不變(invariance):表示List<Orange>List<Fruit>不存在型變關(guān)系。

注:子類(subclass)和子類型(subtype)不是同一個(gè)概念究飞。

先回答文章開頭提出的幾個(gè)問題:

  1. 帶通配符的泛型是什么意思置谦?
    ----這是因?yàn)镴ava泛型本身不支持型變,因此引入通配符來解決泛型類型的類型轉(zhuǎn)換問題亿傅,這是Java型變的通用表達(dá)式(<? extends T>表示類型轉(zhuǎn)換的上界媒峡;<? super T>表示類型轉(zhuǎn)換的下界)。

  2. 如果直接用指定的T會(huì)有什么問題葵擎?
    ----直接使用T作為參數(shù)的類型不會(huì)有任何問題谅阿,但這會(huì)限制函數(shù)接口調(diào)用的靈活性,導(dǎo)致框架的通用性降低酬滤。

  3. 這樣做是為了解決什么問題签餐?
    ----綜上兩點(diǎn),型變處理的最終目的是在保證了運(yùn)行時(shí)類型安全的基礎(chǔ)上盯串,并提高參數(shù)類型的靈活性氯檐。

型變

看一個(gè)不使用型變的例子:

/**
 * 1.定義一個(gè)String類型的List
 */
List<String> value1 = new ArrayList<String>();

/**
 * 2. 這里編譯器報(bào)錯(cuò),因?yàn)閮烧邲]有型變關(guān)系,無法直接賦值体捏,后續(xù)操作會(huì)導(dǎo)致類型不安全
 */
List<Object> value2 = value1; //error


/**
 * 3.假如上面第2步編譯通過了冠摄,那么此時(shí)add()這個(gè)整型數(shù)據(jù)1到value2中
 * 是沒問題的糯崎,因?yàn)樗念愋褪荗bject。但是讀取時(shí)會(huì)碰到困難耗拓,如第4步拇颅。
 */
value2.add(1);

/**
 * 4.但此時(shí)讀出來的是什么類型呢,上一步add了一個(gè)整型數(shù)據(jù)1,此時(shí)如果用String類
 * 型的變量接返回值乔询,肯定不合適樟插,因此運(yùn)行時(shí)會(huì)報(bào)類型轉(zhuǎn)換異常!竿刁!
 */
String result = value1.get(0); //error

上面舉例說明了在不使用型變的情況下黄锤,對泛型數(shù)據(jù)的操作會(huì)面臨種種困難,雖然保證了運(yùn)行時(shí)參數(shù)類型的安全食拜,卻限制了接口的靈活性(編譯器檢查)鸵熟,比如:如果我們只調(diào)用value2(List<Object>)的get()方法,不調(diào)用add()方法(只讀取數(shù)據(jù)不寫入數(shù)據(jù))负甸,顯然此時(shí)不會(huì)有類型的安全問題流强,那如何限制只能調(diào)用get()卻不能add()方法呢?當(dāng)然只能靠編譯器限制了呻待,讓你調(diào)add()方法的時(shí)候編譯都通不過就可以了打月。通配符就是干這件事的,通知編譯器蚕捉,限制我們對于某些方法的調(diào)用奏篙,以保證運(yùn)行時(shí)的類型安全。

協(xié)變

對于上面不型變的例子迫淹,我們可以做如下調(diào)整秘通,就可以達(dá)到協(xié)變的目的:

  /**
 * 2. 這里編譯器不會(huì)報(bào)錯(cuò)
 */
List<? extends String> value2 = value1;

/**
 * 3.但此處編譯器報(bào)錯(cuò)了,編譯器限制了寫入數(shù)據(jù)的操作
 */
value2.add(1); //error

但上面的簡單例子太過簡單敛熬,缺少繼承關(guān)系肺稀,不能明顯說明問題,下面仍以Orange類是Fruit類的子類來舉例說明:

/**
 * 1.定義一個(gè)類型上界限定為Fruit的List应民,即協(xié)變
 */
List<? extends Fruit> fruits = new ArrayList<>();

/**
 * 2.編譯器報(bào)錯(cuò)盹靴,不能添加任何類型的數(shù)據(jù)
 * 原因是:
 * List.add(T t)函數(shù)通過上面的類型指定后,參數(shù)會(huì)變成
 * <? extends Fruit>,從這個(gè)參數(shù)中瑞妇,編譯器無法知道需要哪個(gè)具體的Fruit子類型,
 * Orange梭冠、Banana甚至Fruit都可以辕狰,因此,為了保證類型安全控漠,編譯器拒絕任何類型蔓倍。
 */
//fruits.add(new Orange());
//fruits.add(new Fruit());
//fruits.add(new Object());

/**
 * 3.此處正常P! 由于我們定義是指定了上界為Fruit偶翅,因此此處的返回值肯定至少是Fruit類型默勾,
 * 而基類型可以引用子類型
 */
Fruit f = fruits.get(0);

通過上面代碼的注釋可以看出,協(xié)變限制了參數(shù)中帶T的方法調(diào)用聚谁,比如上面的add(T t)方法(我們稱之為消費(fèi)者方法)母剥,而允許生產(chǎn)者方法的調(diào)用如T get(int position),以此來保證類型的安全形导。

逆變

協(xié)變的反方向是逆變环疼,在協(xié)變中我們可以安全地從泛型類中讀取(從一個(gè)方法中返回)朵耕,而在逆變中我們可以安全地向泛型類中寫入(傳遞給一個(gè)方法)炫隶。

/**
 * 1.定義一個(gè)Object的List,作為原始數(shù)據(jù)列表
 */
List<Object> objects = new ArrayList<>();
objects.add(new Object()); //添加數(shù)據(jù)沒有問題
objects.add(new Orange()); //仍然沒有問題,

/**
 * 2.定義一個(gè)類型下界限定為Fruit的List,并將objects賦值給它阎曹。
 * 此時(shí)編譯不會(huì)報(bào)錯(cuò)伪阶,因?yàn)闈M足逆變的條件。
 */
List<? super Fruit> fruits = objects;

/**
 * 3.add(T t)函數(shù)处嫌,編譯器不會(huì)報(bào)錯(cuò)栅贴,因?yàn)閒ruits接受Fruit的基類類型,
 * 而該類型可以引用其子類型(多態(tài)性)
 */
fruits.add(new Orange());
fruits.add(new Fruit());
fruits.add(new RedApple());

/**
 * 4.此處編譯器報(bào)錯(cuò)锰霜,因?yàn)閒ruits限定的是下界是Friut類型,因此筹误,
 * 編譯器并不知道確切的類型是什么,沒法找到一個(gè)合適的類型接受返回值
 */
Fruit f = fruits.get(0);

/**
 * 5.此處不會(huì)報(bào)錯(cuò)癣缅,因?yàn)镺bject是Fruit的最頂層基類厨剪,滿足下界的限定
 */
//Object obj = fruits.get(0);

通過上面代碼的注釋可以看出,逆變限制了讀取方法的調(diào)用友存,比如上面的T get(int position)方法(我們稱之為生產(chǎn)者方法)祷膳,而允許消費(fèi)者方法的調(diào)用如add(T t),依次來保證類型的安全屡立。

總結(jié)

extends限定了通配符類型的上界直晨,所以我們可以安全地從其中讀取膨俐;而super限定了通配符類型的下界勇皇,所以我們可以安全地向其中寫入。
我們把那些只能從中讀取的對象稱為生產(chǎn)者(Producer)焚刺,我們可以從生產(chǎn)者中安全地讀攘舱;只能寫入的對象稱為消費(fèi)者(Consumer)乳愉。
因此這里就是著名的PECS原則:Producer-Extends, Consumer-Super兄淫。

源碼分析實(shí)戰(zhàn)

結(jié)合上文總結(jié)的PECS原則屯远,來看文章開頭提到的框架源碼(這里就不貼重復(fù)的源碼了),不難看出其含義了:

  1. RxJava中的subscribe(Observer<? super T> observer)函數(shù)由于并沒有返回T類型的數(shù)據(jù)捕虽,因此是一個(gè)消費(fèi)者方法慨丐,根據(jù)PECS原則,此處參數(shù)應(yīng)使用逆變來提高靈活性泄私。
  2. RxJava的map操作符函數(shù)map(Function<? super T, ? extends R> mapper)房揭,他最終的調(diào)用在MapObserver類中的onNext()中執(zhí)行R v = mapper.apply(t),仍根據(jù)PECS原則挖滤,T僅做為傳入?yún)?shù)類型崩溪,因此是個(gè)消費(fèi)者參數(shù),可以使用逆變斩松;而R僅在返回值中出現(xiàn)伶唯,因此是個(gè)生產(chǎn)者參數(shù),可以使用協(xié)變惧盹,來保證類型類型安全乳幸。
  3. java集合框架Collections的工具方法copy(List<? super T> dest, List<? extends T> src),它具體的實(shí)現(xiàn)是如下:
    for (int i=0; i<srcSize; i++) {
        dest.set(i, src.get(i));
    }

可以看出,src調(diào)用了get(i),這是一個(gè)生產(chǎn)者的過程钧椰,因此這里使用了協(xié)變參數(shù)粹断;而dest調(diào)用了set(i, t),這是一個(gè)消費(fèi)者的過程嫡霞,因此這里使用了逆變參數(shù)瓶埋。

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

Java中數(shù)組是協(xié)變的:可以向子類型的數(shù)組賦予基類型的數(shù)組引用,由于數(shù)組在Java中是完全定義的诊沪,因此內(nèi)建了編譯期和運(yùn)行時(shí)的檢查养筒,具體參見如下代碼注釋:

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

/**
 * 創(chuàng)建了一個(gè)Apple數(shù)組,并將其賦值給一個(gè)Fruit數(shù)組引用,編譯器和運(yùn)行時(shí)都允許
 */
Fruit[] fruits = new Apple[10];

/**
 * 將子類對象放置到父類數(shù)組中,編譯器和運(yùn)行時(shí)都允許
 */
fruits[0] = new Apple();
fruits[1] = new Jonathan();

try {
    /**
     * 將Apple的父類對象放置到子類數(shù)組中端姚,編譯器允許晕粪,但運(yùn)行時(shí)檢查拋出異常
     */
    fruits[2] = new Fruit();
} catch (Exception e) {
    Log.i(TAG, "array exception!", e);
}

try {
    /**
     * 將Apple的兄弟對象放置到數(shù)組中,編譯器允許渐裸,但運(yùn)行時(shí)檢查拋出異常
     */
    fruits[3] = new Orange();
} catch (Exception e) {
    Log.i(TAG, "array exception!", e);
}

自限定與協(xié)變

Java中一個(gè)常見的自限定寫法是:

class Base<T extends Base<T>> {
    T element;
    
    T get() {
        return element;
    }
    
    void set(T t) {
        element = t;
    }
}

這種語法定義了一個(gè)基類巫湘,這個(gè)基類能夠使用子類作為其參數(shù)、返回類型昏鹃、作用域尚氛。

  1. 協(xié)變參數(shù)類型
    在非泛型代碼中,參數(shù)類型不能隨子類型發(fā)生變化洞渤。方法只能重載不能重寫怠褐。在使用自限定類型時(shí),方法接受子類型而不是基類型為參數(shù):
/**
 * 自限定協(xié)變參數(shù)類型
 * 方法接受只能接受子類型而不是基類型為參數(shù)
 * @param <T>
 */
interface SetInterface<T extends SetInterface<T>> {

    void set(T arg);
}

/**
 * 具體的子類型
 * 避免重寫基類的方法
 */
interface SubSetInterface extends SetInterface<SubSetInterface> {}


public void test5(SubSetInterface s1, SubSetInterface s2, SetInterface sb) {
    /**
     * 編譯通過
     */
    s1.set(s2);

    /**
     * 只能接受具體的子類型您宪,不能接受SetInterface基類型
     */
    //s1.set(sb); //error
}
  1. 協(xié)變返回類型
    繼承自限定基類的子類奈懒,將產(chǎn)生確切的子類型作為其返回值.不過,這種實(shí)現(xiàn)java的多態(tài)性已經(jīng)可以達(dá)到目的(基類引用子類):
/**
 * 自限定協(xié)變返回類型
 * @param <T>
 */
interface GetInterface<T extends GetInterface<T>> {

    T get();
}


/**
 * 具體的子類型
 * 避免重寫基類的方法
 */
interface SubGetInterface extends GetInterface<SubGetInterface> {}



public void test4(SubGetInterface g) {
    GetInterface s1 = g.get();
    SubGetInterface s2 = g.get();
}

參考文檔

http://www.reibang.com/p/0c2948f7e656
https://www.cnblogs.com/en-heng/p/5041124.html
http://www.reibang.com/p/2bf15c5265c5

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宪巨,一起剝皮案震驚了整個(gè)濱河市磷杏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌捏卓,老刑警劉巖极祸,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怠晴,居然都是意外死亡遥金,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門蒜田,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稿械,“玉大人,你說我怎么就攤上這事冲粤∶滥” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵梯捕,是天一觀的道長厢呵。 經(jīng)常有香客問我,道長傀顾,這世上最難降的妖魔是什么襟铭? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮短曾,結(jié)果婚禮上寒砖,老公的妹妹穿的比我還像新娘。我一直安慰自己错英,他們只是感情好入撒,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著椭岩,像睡著了一般茅逮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上判哥,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天献雅,我揣著相機(jī)與錄音,去河邊找鬼塌计。 笑死挺身,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锌仅。 我是一名探鬼主播章钾,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼墙贱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贱傀?” 一聲冷哼從身側(cè)響起惨撇,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎府寒,沒想到半個(gè)月后魁衙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡株搔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年剖淀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纤房。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡纵隔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帆卓,到底是詐尸還是另有隱情巨朦,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布剑令,位于F島的核電站糊啡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吁津。R本人自食惡果不足惜棚蓄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碍脏。 院中可真熱鬧梭依,春花似錦、人聲如沸典尾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钾埂。三九已至河闰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間褥紫,已是汗流浹背姜性。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留髓考,地道東北人部念。 一個(gè)月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親儡炼。 傳聞我的和親對象是個(gè)殘疾皇子妓湘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

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