Java 泛型 (Generic) 的 Covariance, Contravariance, 以及 Invariance
Covariance,Contravariance,Invariance
把 A、B 想成類別,F 想成某種轉換,如 F(A) 表示把 A 轉換成其他形式,≤ 符號表示子類別關係,如 A ≤ B 表示 A 是 B 的子類別。有這些後,可以簡單說明 Covariance、Contravariance 以及 Invariance 的意思:
- Covariance:A ≤ B 可以得出 F(A) ≤ F(B),則稱 F 具有 Covariance。
- Contravariance:A ≤ B 可以得出 F(B) ≤ F(A),則稱 F 具有 Contravariance。
- Invariance:F 既不是 Covariance 也不是 Contravariance 時,就稱 F 為 Invariance。
Java 中的 Covariance
假設我們定義了以下的類別:FujiApple ≤ Apple ≤ Fruit ≤ Food,Orange ≤ Fruit ≤ Food。稍微熟悉 Java 語法的人,很快就能了解 Java 的 Array 是 Covariance,也就是說 Apple[] 可以是 Fruit []。那我們能利用這樣的特性做什麼事呢?
// 初始化一個 Apple array Apple[] apples = new Apple[10]; // Apple ≤ Food,所以這樣是可以的 Food[] foods = apples; // 從 Food array 取出 Food 本來就沒問題 Food f = foods[0]; // Oh my god !! Error foods[1] = new Orange();從上面的幾行程式碼我們可以看到,把 Apple array 指給 Food array 是沒問題的,因為 Apple 是一種 Food,同理取出 Food array 裡的元素也是沒問題的。最後一行有問題的原因是:我們無法得知實際上 Food array 裡的元素是什麼類別。
以上面的例子來說,Food array 裡的元素形態實際上是 Apple ,但我們卻想把 Orange 寫入 array,這樣當然會有錯誤。你當然可以在知道實際類別的情形下做類別轉換,但更多是不知道類別的情形。因此在利用 Covariance 時,盡量只做讀取而不做寫入。
Java 中的 Contravariance
Java 是不支援 Contravariance 的,但還是可以假設 Java 有支援來討論 Contravariance 能幹麻。依照上面的定義,就是 Food 可以是 Apple,那能做什麼事呢?
// 初始化一個 Fruit array
Fruit[] fruits = new Fruit[] {new Apple(), new Orange(), new FujiApple()};
// Food ≤ Apple,所以這樣是可以的
Apple[] apples = fruits;
// FujiApple 是一種 Apple,所以可以寫入 Apple array
apples[0] = new FujiApple();
// Oh my god !! Error
Apple apple = apples[1];
從上面的程式碼可以看到,假設 Contravariance 成立時,Food array 指給 Apple array 是沒問題的,而寫入 Apple array 也沒什麼問題,只要確保是寫入 Apple 類別或 Apple 的子類別就可以,但讀取的時候就可能發生問題:我們無法得知實際上存在 Apple array 裡的元素是否全部都是 Apple。因此在利用 Contravariance 時,盡量只做寫入而不做讀取。Java 中的 Invariance
最常使用的地方是在泛型 (Generic)。我們都知道從編譯器會報錯就知道 List<Apple> 跟 List<Fruit> 是不能互相指向的。當能互相指向時會發生什麼事呢?List<Apple> apples = new ArrayList<Apple>(); // Covariance List<Fruit> fruits = apple; // Orange ≤ Fruit,可以加進 list fruits.add(new Orange()); // 取出來的到底是 Apple 還是 Orange? Apple apple = apples.get(0);既然 Java 的泛型是 Invariance,好像就少了一些彈性,有沒有什麼方式能讓泛型能具有 Covariance 或是 Contravariance 呢?
Java 中的 wildcards
wildcards 讓 Java 中的泛型可以達到具有 Covariance 或是 Contravariance 的效果:
- List <? extends E>:表示 E 是類別的上限,任意類別都能轉換成 E。
- List <? super E>:表示 E 是類別的下限,E 都能轉換成父類別。
// 以下三個序述都成立,只要是 Fruit 的子類別都可以 List<? extends Fruit> fruits = new ArrayList<FujiApple>(); List<? extends Fruit> fruits = new ArrayList<Apple>(); List<? extends Fruit> fruits = new ArrayList<Orange>();上面的 code 可以看出 <? extends Fruit> 具有 covariance,因此不管 fruits 實際指向的是什麼 List,我們取出 List 元素都能確保是 Fruit,但寫入就會有問題,因為不確定要寫入的是什麼類別。
// 以下三個敘述都成立,只要是 Apple 的父類別都可以 List<? super Apple> apples = new ArrayList<Apple>(); List<? super Apple> apples = new ArrayList<Fruit>(); List<? super Apple> apples = new ArrayList<Food>();上面的 code 可以看出 <? super Apple> 具有 Contravariance。寫入時只要是寫入 Apple 或是 Apple 字類別都可以 (因為可能指向 Apple / Fruit / Food List,要確保寫入的類別都能轉換成這些型態),但讀取就會有問題,因為不確定 apples List 原本指向的是什麼類別。
PECS (Producer Extends, Consumer super)
當你要對一個 Collection 只讀取而不加入元素時,這個 Collection 就是 Producer,這時候就要用 <? extends E>;當你要對一個 Collection 只加入元素而不讀取時,這個 Collection 就是 Consumer,這時候就要用 <? super E>。當你要對 Collection 又加入又讀取時,這個 Collection 既是 Producer 也是 Consumer,這時就要指定 Collection 類別,如 List<Apple>。
Java source example
可以參考 Collections.copy
/** * Copies all of the elements from one list into another. After the * operation, the index of each copied element in the destination list * will be identical to its index in the source list. The destination * list must be at least as long as the source list. If it is longer, the * remaining elements in the destination list are unaffected. * * This method runs in linear time. * * @param可以看到參數中的 src 型態是 List<? extends T>,限制了 src 只能用來讀取,而 dest 型態是 List<? super T>,限制了 dest 只能寫入元素。the class of the objects in the lists * @param dest The destination list. * @param src The source list. * @throws IndexOutOfBoundsException if the destination list is too small * to contain the entire source List. * @throws UnsupportedOperationException if the destination list's * list-iterator does not support the set operation. */ 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()); } } }
參考資料
- StackOverflow - Covariance, Invariance and Contravariance explained in plain English?
- StackOverflow - Why are arrays covariant but generics are invariant?
- StackOverflow - Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?
- StackOverflow - What is PECS (Producer Extends Consumer Super)?
- Java wildcards
- Java中的协变与逆变
留言
張貼留言