Java中的逆變與協變

Java中的逆變與協變

原文: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中的逆變和協變以及泛型中通配符用法。

1. 逆變與協變

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

Liskov替換原則

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

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

LSP包含以下四層含義:

子類完全擁有父類的方法,且具體子類必須實現父類的抽象方法。

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

當子類覆蓋或實現父類的方法時,方法的形參要比父類方法的更為寬松。

當子類覆蓋或實現父類的方法時,方法的返回值要比父類更嚴格。

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

Number num =newInteger(1);

定義

逆變與協變用來描述類型轉換(type transformation)后的繼承關系,其定義:如果AA、BB表示類型,f(?)f(?)表示類型轉換,≤≤表示繼承關系(比如,A≤BA≤B表示AA是由BB派生出來的子類);

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

f(?)f(?)是協變(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)相互之間沒有繼承關系。

類型轉換

接下來,我們看看Java中的常見類型轉換的協變性、逆變性或不變性。

泛型

令f(A)=ArrayList,那么f(?)f(?)時逆變、協變還是不變的呢?如果是逆變,則ArrayList是ArrayList的父類型;如果是協變,則ArrayList是ArrayList的子類型;如果是不變,二者沒有相互繼承關系。開篇代碼中用ArrayList實例化list的對象錯誤,則說明泛型是不變的。

數組

令f(A)=[]A,容易證明數組是協變的:

Number[] numbers =newInteger[3];

方法

方法的形參是協變的、返回值是逆變的:

通過與網友iamzhoug37的討論,更新如下。

調用方法result = method(n);根據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開始,子類覆蓋父類方法時允許協變返回更為具體的類型:

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

2. 泛型中的通配符

實現泛型的協變與逆變

Java中泛型是不變的,可有時需要實現逆變與協變,怎么辦呢?這時,通配符?派上了用場:

實現了泛型的協變,比如:

List list =newArrayList();

實現了泛型的逆變,比如:

List list =newArrayList();

extends與super

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

publicinterfaceListextendsCollection{boolean add(E e);}

在調用add方法時,泛型E自動變成了,其表示list所持有的類型為在Number與Number派生子類中的某一類型,其中包含Integer類型卻又不特指為Integer類型(Integer像個備胎一樣!!!),故add?Integer時發生編譯錯誤。為了能調用add方法,可以用super關鍵字實現:

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

表示list所持有的類型為在Number與Number的基類中的某一類型,其中Integer與Float必定為這某一類型的子類;所以add方法能被正確調用。從上面的例子可以看出,extends確定了泛型的上界,而super確定了泛型的下界。

PECS

現在問題來了:究竟什么時候用extends什么時候用super呢?《Effective Java》給出了答案:

PECS: producer-extends, consumer-super.

比如,一個簡單的Stack API:

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

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

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

假設有一個實例化Stack的對象stack,src有Iterable與?Iterable;在調用pushAll方法時會發生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);}

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

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

同樣地,假設有一個實例化Stack的對象stack,dst為Collection;調用popAll方法是會發生type mismatch錯誤,因為Collection不是Collection的子類型。因而,應改為:

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

在上述例子中,在調用pushAll方法時生產了E 實例(produces E instances),在調用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總結:

要從泛型類取數據時,用extends;

要往泛型類寫數據時,用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.

如需轉載,請注明作者及出處.

作者:Treant

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

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

推薦閱讀更多精彩內容

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