Java中的逆變與協變
導讀 | 有人會納悶,為什麼Number的物件可以由Integer例項化,而ArrayList的物件卻不能由ArrayList例項化?list中的宣告其元素是Number或Number的派生類,為什麼不能add Integer和Float?為了解決這些問題,我們需要了解Java中的逆變和協變以及泛型中萬用字元用法。 |
在介紹逆變與協變之前,先引入Liskov替換原則(Liskov Substitution Principle, LSP)。
LSP由Barbara Liskov於1987年提出,其定義如下:
所有引用基類(父類)的地方必須能透明地使用其子類的物件。
LSP包含以下四層含義:
- 子類完全擁有父類的方法,且具體子類必須實現父類的抽象方法。
- 子類中可以增加自己的方法。
- 當子類覆蓋或實現父類的方法時,方法的形參要比父類方法的更為寬鬆。
- 當子類覆蓋或實現父類的方法時,方法的返回值要比父類更嚴格。
前面的兩層含義比較好理解,後面的兩層含義會在下文中詳細解釋。根據LSP,我們在例項化物件的時候,可以用其子類進行例項化,比如:
Number num = new Integer(1);
逆變與協變用來描述型別轉換(type transformation)後的繼承關係,其定義:如果A、B表示型別,f(⋅)表示型別轉換,≤表示繼承關係(比如,A≤B表示A是由B派生出來的子類);
接下來,我們看看Java中的常見型別轉換的協變性、逆變性或不變性。
泛型:
令f(A)=ArrayList,那麼f(⋅)時逆變、協變還是不變的呢?如果是逆變,則ArrayList是ArrayList的父型別;如果是協變,則ArrayList是ArrayList的子型別;如果是不變,二者沒有相互繼承關係。開篇程式碼中用ArrayList例項化list的物件錯誤,則說明泛型是不變的。
陣列:
令f(A)=[]A,容易證明陣列是協變的:
Number[] numbers = new Integer[3];
呼叫方法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) { return 1; } Object result = method(new Integer(2)); //correct Number result = method(new Object()); //error Integer result = method(new Integer(2)); //error
在Java 1.4中,子類覆蓋(override)父類方法時,形參與返回值的型別必須與父類保持一致:
class Super { Number method(Number n) { ... } } class Sub extends Super { @Override Number method(Number n) { ... } }
從Java 1.5開始,子類覆蓋父類方法時允許協變返回更為具體的型別:
class Super { Number method(Number n) { ... } } class Sub extends Super { @Override Integer method(Number n) { ... } }
Java中泛型是不變的,可有時需要實現逆變與協變,怎麼辦呢?這時,萬用字元?派上了用場:
實現了泛型的協變,比如:
List list = new ArrayList();
實現了泛型的逆變,比如:
List list = new ArrayList();
為什麼(開篇程式碼中)List list在add Integer和Float會發生編譯錯誤?首先,我們看看add的實現:
public interface Listextends Collection{ boolean add(E e); }
在呼叫add方法時,泛型E自動變成了,其表示list所持有的型別為在Number與Number派生子類中的某一型別,其中包含Integer型別卻又不特指為Integer型別(Integer像個備胎一樣!!!),故add Integer時發生編譯錯誤。為了能呼叫add方法,可以用super關鍵字實現:
List list = new ArrayList(); list.add(new Integer(1)); list.add(new Float(1.2f));
表示list所持有的型別為在Number與Number的基類中的某一型別,其中Integer與Float必定為這某一型別的子類;所以add方法能被正確呼叫。從上面的例子可以看出,extends確定了泛型的上界,而super確定了泛型的下界。
現在問題來了:究竟什麼時候用extends什麼時候用super呢?《Effective Java》給出了答案:
PECS: producer-extends, consumer-super.
比如,一個簡單的Stack API:
public class Stack{ public Stack(); public void push(E e): public E pop(); public boolean isEmpty(); }
要實現pushAll(Iterable src)方法,將src的元素逐一入棧:
public void pushAll(Iterablesrc){ 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 producer public 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(Collectiondst) { while (!isEmpty()) dst.add(pop()); }
同樣地,假設有一個例項化Stack的物件stack,dst為Collection;呼叫popAll方法是會發生type mismatch錯誤,因為Collection不是Collection的子型別。因而,應改為:
// Wildcard type for parameter that serves as an E consumer public 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: public staticvoid copy(List dest, List 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<1srcsize; i++)="" dest.set(i,="" src.get(i));="" }="" else="" {="" listiteratordi=dest.listIterator(); ListIterator si=src.listIterator(); for (int i=0; i<1srcsize; i++)="" {="" di.next();="" di.set(si.next());="" }=""
PECS總結:
原文來自:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2684914/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- C#中的協變和逆變C#
- .NET C#雜談(1):變體 - 協變、逆變與不變C#
- 詳解C#的協變和逆變C#
- 瞭解C#的協變和逆變C#
- 你瞭解C#的協變和逆變嗎C#
- 【C#開發】C#的協變和逆變C#
- C#泛型的逆變協變(個人理解)C#泛型
- .NET Core CSharp初級篇 1-8泛型、逆變與協變CSharp泛型
- C#基礎筆記——協變(Covariance)和逆變(Contravariance)C#筆記
- 重學c#系列——逆變和協變[二十四]C#
- iOS-關鍵字-泛型ObjectType 協變__covariant 逆變__contravariantiOS泛型Object
- 泛型協變與抗變(二)泛型
- 【OpenCV-Python】:影像的傅立葉變換與逆傅立葉變換OpenCVPython
- IP協議的發展與演變協議
- java中變數的作用域Java變數
- 泛型、陣列列表與協變泛型陣列
- Java區域性變數與全域性變數Java變數
- 翻譯 | Java 中的變型(Variance)Java
- Java 的可變引數與 Collections 類Java
- 二,Java中常量與變數的理解Java變數
- JPEG格式研究——(4)反量化、逆ZigZag變化和IDCT變換
- C#深入學習:泛型修飾符in,out、逆變委託型別和協變委託型別C#泛型型別
- Java入門系列-04-java中的變數Java變數
- 【Java貓說】例項變數與區域性變數Java變數
- 深入理解Java中的不可變物件Java物件
- 使用 Java 讀寫 JMeter 中的變數JavaJMeter變數
- Java中的不可變資料結構Java資料結構
- Java中如何快捷的建立不可變集合Java
- C# - 逆變的具體應用場景C#
- Java中實現不可變MapJava
- 使用世界變換的逆轉置矩陣對法線進行變換矩陣
- JAVA基礎04——變數與常量Java變數
- 前後端分離時代,Java 程式設計師的變與不變!後端Java程式設計師
- 天外風景——黑曜石的變與不變
- Java培訓基礎知識-Java的常量與變數Java變數
- Java中變數之區域性變數、本類成員變數、父類成員變數的訪問方法Java變數
- 逆變器的防孤島測試效能評估
- 光伏逆變器負載的功能和優勢負載