Java中的逆變與協(xié)變

Java中的逆變與協(xié)變

原文:http://www.cnblogs.com/en-heng/p/5041124.html

看下面一段代碼

Number num =newInteger(1);? ArrayList list =newArrayList();//type mismatchList list =newArrayList();list.add(newInteger(1));//errorlist.add(newFloat(1.2f));//error

有人會納悶韵丑,為什么Number的對象可以由Integer實例化膊存,而ArrayList的對象卻不能由ArrayList實例化嵌纲?list中的聲明其元素是Number或Number的派生類可款,為什么不能add?Integer和Float?為了解決這些問題敏晤,我們需要了解Java中的逆變和協(xié)變以及泛型中通配符用法击碗。

1. 逆變與協(xié)變

在介紹逆變與協(xié)變之前鸡岗,先引入Liskov替換原則(Liskov Substitution Principle, LSP)鸳劳。

Liskov替換原則

LSP由Barbara Liskov于1987年提出,其定義如下:

所有引用基類(父類)的地方必須能透明地使用其子類的對象宝鼓。

LSP包含以下四層含義:

子類完全擁有父類的方法刑棵,且具體子類必須實現(xiàn)父類的抽象方法。

子類中可以增加自己的方法愚铡。

當子類覆蓋或?qū)崿F(xiàn)父類的方法時蛉签,方法的形參要比父類方法的更為寬松。

當子類覆蓋或?qū)崿F(xiàn)父類的方法時沥寥,方法的返回值要比父類更嚴格碍舍。

前面的兩層含義比較好理解,后面的兩層含義會在下文中詳細解釋营曼。根據(jù)LSP乒验,我們在實例化對象的時候,可以用其子類進行實例化蒂阱,比如:

Number num =newInteger(1);

定義

逆變與協(xié)變用來描述類型轉(zhuǎn)換(type transformation)后的繼承關(guān)系锻全,其定義:如果AA、BB表示類型录煤,f(?)f(?)表示類型轉(zhuǎn)換鳄厌,≤≤表示繼承關(guān)系(比如,A≤BA≤B表示AA是由BB派生出來的子類)妈踊;

f(?)f(?)是逆變(contravariant)的了嚎,當A≤BA≤B時有f(B)≤f(A)f(B)≤f(A)成立;

f(?)f(?)是協(xié)變(covariant)的廊营,當A≤BA≤B時有f(A)≤f(B)成立f(A)≤f(B)成立歪泳;

f(?)f(?)是不變(invariant)的,當A≤BA≤B時上述兩個式子均不成立露筒,即f(A)f(A)與f(B)f(B)相互之間沒有繼承關(guān)系呐伞。

類型轉(zhuǎn)換

接下來,我們看看Java中的常見類型轉(zhuǎn)換的協(xié)變性慎式、逆變性或不變性伶氢。

泛型

令f(A)=ArrayList,那么f(?)f(?)時逆變瘪吏、協(xié)變還是不變的呢癣防?如果是逆變,則ArrayList是ArrayList的父類型掌眠;如果是協(xié)變蕾盯,則ArrayList是ArrayList的子類型;如果是不變蓝丙,二者沒有相互繼承關(guān)系级遭。開篇代碼中用ArrayList實例化list的對象錯誤香嗓,則說明泛型是不變的。

數(shù)組

令f(A)=[]A装畅,容易證明數(shù)組是協(xié)變的:

Number[] numbers =newInteger[3];

方法

方法的形參是協(xié)變的、返回值是逆變的:

通過與網(wǎng)友iamzhoug37的討論沧烈,更新如下掠兄。

調(diào)用方法result = method(n);根據(jù)Liskov替換原則锌雀,傳入形參n的類型應為method形參的子類型蚂夕,即typeof(n)≤typeof(method's parameter);result應為method返回值的基類型腋逆,即typeof(methods's return)≤typeof(result):

static Number method(Number num){return1;}Object result =method(newInteger(2));//correctNumber result =method(newObject());//errorInteger result =method(newInteger(2));//error

在Java 1.4中婿牍,子類覆蓋(override)父類方法時,形參與返回值的類型必須與父類保持一致:

classSuper{Number method(Number n){... }}classSubextendsSuper{@OverrideNumber method(Number n){... }}

從Java 1.5開始惩歉,子類覆蓋父類方法時允許協(xié)變返回更為具體的類型:

classSuper{Number method(Number n){... }}classSubextendsSuper{@OverrideInteger method(Number n){... }}

2. 泛型中的通配符

實現(xiàn)泛型的協(xié)變與逆變

Java中泛型是不變的等脂,可有時需要實現(xiàn)逆變與協(xié)變,怎么辦呢撑蚌?這時上遥,通配符?派上了用場:

實現(xiàn)了泛型的協(xié)變,比如:

List list =newArrayList();

實現(xiàn)了泛型的逆變争涌,比如:

List list =newArrayList();

extends與super

為什么(開篇代碼中)List list在add?Integer和Float會發(fā)生編譯錯誤粉楚?首先,我們看看add的實現(xiàn):

publicinterfaceListextendsCollection{boolean add(E e);}

在調(diào)用add方法時亮垫,泛型E自動變成了模软,其表示list所持有的類型為在Number與Number派生子類中的某一類型,其中包含Integer類型卻又不特指為Integer類型(Integer像個備胎一樣R省H家臁!)害晦,故add?Integer時發(fā)生編譯錯誤特铝。為了能調(diào)用add方法,可以用super關(guān)鍵字實現(xiàn):

List list =newArrayList();list.add(newInteger(1));list.add(newFloat(1.2f));

表示list所持有的類型為在Number與Number的基類中的某一類型壹瘟,其中Integer與Float必定為這某一類型的子類鲫剿;所以add方法能被正確調(diào)用。從上面的例子可以看出稻轨,extends確定了泛型的上界灵莲,而super確定了泛型的下界。

PECS

現(xiàn)在問題來了:究竟什么時候用extends什么時候用super呢殴俱?《Effective Java》給出了答案:

PECS: producer-extends, consumer-super.

比如政冻,一個簡單的Stack API:

publicclassStack{publicStack();public void push(E e):public E pop();public boolean isEmpty();}

要實現(xiàn)pushAll(Iterable src)方法枚抵,將src的元素逐一入棧:

public void pushAll(Iterable src){for(E e : src)push(e)}

假設(shè)有一個實例化Stack的對象stack,src有Iterable與?Iterable明场;在調(diào)用pushAll方法時會發(fā)生type mismatch錯誤汽摹,因為Java中泛型是不可變的,Iterable與?Iterable都不是Iterable的子類型苦锨。因此逼泣,應改為

// Wildcard type for parameter that serves as an E producerpublic void pushAll(Iterable src){for(E e : src)push(e);}

要實現(xiàn)popAll(Collection dst)方法,將Stack中的元素依次取出add到dst中舟舒,如果不用通配符實現(xiàn):

// popAll method without wildcard type - deficient!public void popAll(Collection dst){while(!isEmpty())? ? ? ? dst.add(pop());? }

同樣地拉庶,假設(shè)有一個實例化Stack的對象stack,dst為Collection秃励;調(diào)用popAll方法是會發(fā)生type mismatch錯誤氏仗,因為Collection不是Collection的子類型。因而夺鲜,應改為:

// Wildcard type for parameter that serves as an E consumerpublic void popAll(Collection dst){while(!isEmpty())? ? ? ? dst.add(pop());}

在上述例子中皆尔,在調(diào)用pushAll方法時生產(chǎn)了E 實例(produces E instances),在調(diào)用popAll方法時dst消費了E 實例(consumes E instances)谣旁。Naftalin與Wadler將PECS稱為Get and Put Principle床佳。

java.util.Collections的copy方法(JDK1.7)完美地詮釋了PECS:

publicstaticvoid copy(List dest, List src){intsrcSize = src.size();if(srcSize > dest.size())thrownewIndexOutOfBoundsException("Source does not fit in dest");if(srcSize < COPY_THRESHOLD ||? ? ? ? (srcinstanceofRandomAccess && destinstanceofRandomAccess)) {for(inti=0; i di=dest.listIterator();? ? ? ? ListIterator si=src.listIterator();for(inti=0; i

PECS總結(jié):

要從泛型類取數(shù)據(jù)時,用extends榄审;

要往泛型類寫數(shù)據(jù)時砌们,用super;

既要取又要寫搁进,就不用通配符(即extends與super都不用)浪感。

3. 參考資料

[1] meriton,?Covariance, Invariance and Contravariance explained in plain English?.

[2] Bert F,?Difference between and in Java.

[3] Joshua Bloch, Effective Java.

如需轉(zhuǎn)載,請注明作者及出處.

作者:Treant

出處:http://www.cnblogs.com/en-heng/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饼问,一起剝皮案震驚了整個濱河市影兽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌莱革,老刑警劉巖峻堰,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盅视,居然都是意外死亡捐名,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門闹击,熙熙樓的掌柜王于貴愁眉苦臉地迎上來镶蹋,“玉大人,你說我怎么就攤上這事『毓椋” “怎么了淆两?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拂酣。 經(jīng)常有香客問我秋冰,道長,這世上最難降的妖魔是什么婶熬? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任丹莲,我火速辦了婚禮,結(jié)果婚禮上尸诽,老公的妹妹穿的比我還像新娘。我一直安慰自己盯另,他們只是感情好性含,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鸳惯,像睡著了一般商蕴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芝发,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天绪商,我揣著相機與錄音,去河邊找鬼辅鲸。 笑死格郁,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的独悴。 我是一名探鬼主播例书,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼刻炒!你這毒婦竟也來了决采?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坟奥,失蹤者是張志新(化名)和其女友劉穎树瞭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爱谁,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡晒喷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了管行。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厨埋。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荡陷,到底是詐尸還是另有隱情雨效,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布废赞,位于F島的核電站徽龟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏唉地。R本人自食惡果不足惜据悔,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望耘沼。 院中可真熱鬧极颓,春花似錦、人聲如沸群嗤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狂秘。三九已至骇径,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間者春,已是汗流浹背破衔。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钱烟,地道東北人晰筛。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像拴袭,于是被迫代替她去往敵國和親传惠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

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

  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分,“神秘”的通配符泰佳,讓我看了幾遍《Java...
    珞澤珈群閱讀 7,855評論 12 51
  • 對象的創(chuàng)建與銷毀 Item 1: 使用static工廠方法盼砍,而不是構(gòu)造函數(shù)創(chuàng)建對象:僅僅是創(chuàng)建對象的方法,并非Fa...
    孫小磊閱讀 1,986評論 0 3
  • 第8章 泛型 通常情況的類和函數(shù)逝她,我們只需要使用具體的類型即可:要么是基本類型浇坐,要么是自定義的類。但是在集合類的場...
    光劍書架上的書閱讀 2,150評論 6 10
  • 泛型 泛型(Generic Type)簡介 通常情況的類和函數(shù),我們只需要使用具體的類型即可:要么是基本類型,要么...
    Tenderness4閱讀 1,419評論 4 2
  • 編者寄語:你若有顆善良的心,那便更需要一副強有力的鎧甲去保護它案淋。 最近座韵,我讀完了八月長安的小說《你好,舊時光》踢京。 ...
    左夕物語閱讀 372評論 0 1