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   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());
        }
    }
}
        可以看到參數中的 src 型態是 List<? extends T>,限制了 src 只能用來讀取,而 dest 型態是 List<? super T>,限制了 dest 只能寫入元素。

參考資料


留言

這個網誌中的熱門文章

transient 介紹

為什麼 GUI framework 大多都是 single-threaded (not thread safe)