協變與逆變:Kotlin 和 Java 泛型的深入探討
協變(covariance)、逆變(contravariance)和不變(invariance)
Java 的泛型型態是不變的(invariant),意即 List<String>
不是 List<Object>
的子類別。
// Java
ArrayList<String> strs = new ArrayList<String>();
ArrayList<Object> objs = strs; // 這裡編譯器會跟你抗議
objs.add(1);
String s = strs.get(0); // 不然這裡應該拿到什麼?
但是會造成一些合理的操作受到限制:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}void copyAll(Collection<Object> to, Collection<String> from) {
// 編譯器錯誤:Collection<String> 不是 Collection<Object> 的子類別
to.addAll(from);
}
所以,實際的實作是這樣的:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
這樣下來 items
可以是 ? extends E
(通配符類型 wildcard type),用聽起來比較厲害的方式說,指定了上邊界的通配符類型,使它成為一種協變的(covariant)類型 。任何 E
的子類別。我們可以放心從 items
裡面取值(必定是 instanceof E
),但不能寫入 items
(不過也並不代表 items 就是 immutable)。
另一種情況成為逆變(contravariance),比如 List<? super String>
。
Joshua Bloch(Google 的首席 Java 架構師)稱只能讀取的對象為生產者(Producer),只能寫入的對象為消費者(Consumer)。
口訣 PECS:Producer extends, Consumer super.
宣告處類型變異 (Declaration-site variance)
但今天一個泛型即使只提供傳出而沒有任何傳入,如下列 Producer<String>
指派給 Producer<Object>
其實是很安全的,因為一個 String
生產者理所當然也是一個很好的 Object
生產者,但編譯器並不知道。
// Java
interface Producer<T> {
T produce(); // 身為生產者,我只提供拿的方法
}void demo(Producer<String> strProducer) {
Producer<Object> objProducer = strProducer; // 還是會發生編譯錯誤
}
要修好這個錯誤,你可能要把 objects
定義成 List<? extends Object>
,但這種寫法除了避開問題以外意義不大,也沒帶來什麼好處:它和 List<Object>
能呼叫的方法沒什麼區別。
在 Kotlin,我們可以使用宣告處類型變異(declaration-site variance) :
interface Producer<out T> {
fun nextT(): T
}fun demo(strProducer: Producer<String>) {
val objProducer: Producer<Any> = strProducer // OKder
}
此處 out
是一個變異標註(variance annotation),讓類型參數變成協變的(covariant),能夠確保 T
只處現在輸出位置,當它被放到輸入位置時就會發生編譯器錯誤。
上述出現在宣告處的用法稱作宣告處類型變異(declaration-site variance),相較於前述 Java 的用法為使用處類型變異(use-site variance)。
除了 out
以外還有另一種變異標註 in
,用來讓類型參數變成逆變的(contravariant)。
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // Double 是一個 Number
val y: Comparable<Double> = x // OKder
}
於是 PECS 口訣現在可以變成:
Consumer in, Producer out!
類型投射(Type projections)
宣告處類型變異 (Declaration-site variance) 的類型投射(Type projections)
舉例來說你寫了一個複製陣列的函數:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices) {
to[i] = from[i]
}
}
還是會碰到老問題:
val intArray: Array<Int> = arrayOf(1, 2, 3)
val anyArray = Array<Any>(3) { "" }
// 合理,但出現錯誤:from 是 Array<Int> 而不是 Array<Any>
copy(from = intArray, to = anyArray)
上述操作把 Array<Int>
拷貝到 Array<Any>
是合情合理的事,但我們也要確保以下操作是不合法的:
val intArray: Array<Int> = arrayOf(1, 2, 3)
val anyArray = Array<Any>(3) { "" }
// 錯誤,而且必須要出現錯誤:我們正在把 Any 塞進 Int 陣列裡!
copy(from = anyArray, to = intArray)
於是你的函數應當宣告成:
fun copy(from: Array<out Any>, to: Array<Any>) { ... }
這種宣告在 Kotlin 稱作類型投射(Type projection),這裡的涵義是 from
不只是普通的陣列,而是一個被限制(投射)的陣列。這樣一來,我們只能從 from
取值,而不能寫入。
你也可以使用 in
做類型投射:
fun fill(dest: Array<in String>, value: String) { ... }
星號投射(Star projections)
星號投射可以自動根據類別宣告進行投射,讓變數宣告時更方便;有點類似 Java 的原始類型(raw types),但是相較安全。
- 協變的(covariant)
Foo<out T : TUpper>
,Foo<*>
相當於Foo<out TUpper>
,意即當T
未知時可以安全從中讀取TUpper
。 - 逆變的(contravariant)
Foo<in T>
,Foo<*>
相當於Foo<in Nothing>
,意即當T
未知時你無法安全地寫入。 - 不變的(invariant)
Foo<T : TUpper>
,Foo<*>
讀取時相當於Foo<out TUpper>
,寫入時相當於Foo<in Nothing>
。