Covariance and Contravariance in Generics


簡介 Java 和 Kotlin 中泛型的 covariance 與 contravariance (反變數)

在開發中你可能遇過下列這種情境:


  • 創建兩個功能很類似的 Class,唯一的差別在於 Class 中實作的型態 (type) 不同
  • 例如有兩個票匭的 Class,但唯一的差別是一個裡面裝總統選票,另一個裝的是立委選票
  • 初期你可能想借助 Ctrl+C、Ctrl+V 大法,再修改裡面的型態就好
  • 但要再新增公投票匭的話怎麼辦?故技重施!
  • 但當票匭的規格有改,我們就要一再去調整那些很類似的 Class...Orz
  • 有沒有更合理的做法?

別管 Ctrl+C 了,你聽過泛型嗎?

上述實作多個類似 Class 的需求很適合用泛型的設計模式來解決!泛型是指將類別中的型態參數化至實作時才宣告,優點是利於 code reuse,並且可以在編譯時就檢查出型態錯誤

沒有泛型的世界

在沒有泛型的世界裡我們當然可以用 Ctrl+C、Ctrl+V 行走天下,只要出來混總是要還的,當 code base 越來越大,總會出現類似的 Class 需要新增或是改寫裡面的 function,就要確保相關的 Class 都有改好改滿。但透過泛型可以將型態參數化等到實作時才定義,避免重寫類似的 Class 並提高 code reuse。另外泛型也可以在編譯階段就檢查出型態錯誤,而不是等到 runtime 時才出現 ClassCastException 的錯誤。

型態 (type) 都幾?

泛型中的型態如何定義?我們再看回剛剛選舉票匭的例子。以下以 Java 和 Kotlin 為例創建一個 Box 的類別,但我們透過參數 T 代表 Box 的型態,等後續實作時再定義。以 Java 和 Kotlin 為例兩者都是強型別的語言,不同的是 Java 是先宣告型態, Kotlin 的型態則是宣告在變數後,Kotlin 還具有隱性型別的功能,語言會透過變數數值去推斷變數型態。在 Java 中所有定義的 Reference Type 都是繼承自 Object,但在 Kotlin 中不管是 Reference Type 還是 Value Type,最底層的 root type 都是 Any。

public class Box<T> {

   private T t;

   public void set(T t) { this.t = t; }

   public T get() { return t; }
}

public class Box<T> {

   private var t: T? = null

   fun set(t: T) { this.t = t }

   fun get(): T? = t
}

參數型態

泛型參數型態中常見的是角括號中包著參數型態如 ,剛接觸泛型時也許會覺得角括號包著參數的寫法看起來很恐怖,但其實 Java 中常用到的幾個類別如 List、Map<K, V>和 Set 也有其他大寫的字母代表不同意義。

E - Element (used extensively by the Java Collections Framework)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types

The most commonly used type parameter names

常見的泛型參數型態有三種 Invariance、Covariance 和 Contravariance。

Invariance

  • Java, Kotlin:
  • Set value and Get value

Invariance 是最常見的泛型參數型態,Invariance 代表不變數,代表實作時角括號內的型態只能是定義的那個 type。Invariance 可以避免 heap pollution,heap pollution 指的就是將一個未定參數型態的 Collection 物件,指定給一個有指定參數型態的 Collection 物件後,所隱藏的潛在問題, heap pollution 的錯誤會到 runtime 時才會出現 ClassCastException,在編譯階段卻無法偵測,使用 Invariance 的參數型態就可以避免這種錯誤。但若是參數型態間具有 Super-Class 或是 Sub-Class 的關係,為了提高 code reuse 可以透過下列 Covariance 和 Contravariance 的變數。

Covariance

  • Java: <? extends T>, Kotlin:
  • Get value only

Covariance 為反變數,代表實作時可以傳入 T 以及 T 的 sub-class。因為傳入的是 T 及 T 的 sub-class,例如 <? extends Int> 就可以傳入 或是 的參數型態 ( 因為 Number 是 Int 的 sub-class )。T 的角色類似生產者,提供一組介面給未知的 Class 繼承,因為不能確定那些繼承自 T 的未知 Class 如何實作,因此 Covariance 只能從 T 身上 Get value,但不能 Set value。與之相反的就是 Contravariance。

Contravariance

  • Java: <? super T>, Kotlin:
  • Set value only

Contravariance 為反變數,代表實作時可以傳入 T 以及 T 的 super-class。例如 <? super Number> 可以傳入 和 。型態參數 T 的角色類似消費者,會實作未知 super class。由於並不確定未知的 super class 如何定義,所以無法 Get value,但因為知道後來實作的 type 為 T,所以可以 Set value。

Producer out, Consumer in

通過角括號中 T 的角色為 Producer 或是 Consumer,就可以了解什麼時候用 out 和 extends?什麼時候用 in 和 super?如果忘記的話別擔心,compilier 也會貼心提醒說母湯喔~

Reference

#generictype #covariance #contravariance
透過分享開發過程中遇過的一些問題、當中的思考過程和最後的解法,期待能和更多人一起集思廣益!






Related Posts

利用 Docker Compose 管理多個容器

利用 Docker Compose 管理多個容器

前端也能玩 Deep learning - 以 p5-deeplearn-js 為例

前端也能玩 Deep learning - 以 p5-deeplearn-js 為例

[筆記] 模組化與 Library (require & export)

[筆記] 模組化與 Library (require & export)



Sponsored



Comments