泛型基礎 (二) — Java 與 Kotlin 的變型點與 Wildcard 的使用
在介紹泛型與變型時,我有提到在 Java 與 Kotlin 中他們是如何被定義與使用。這篇我們來談談在 Java 與 Kotlin 中定義變型的時機吧。
如果還不知道什麼事變型 (Variance) 的,歡迎參考泛型基礎的第一篇。
繼 Java 與 Kotlin 在定義泛型與變型的文章,我們知道他們在如何定義上有所差異:
List <T> // Java 與 Kotlin 的 GenericList <? extends T> // Java Covariance read only
List <out T> // Kotlin CovarianceList <? super T> // Java Contravariance write only
List <in T> // Kotlin Contravariance
但是你可知道定義變型也講究使用點嗎?
在 Java 中,我們無法使用 wildcard 來定義型別的泛型:
class MyClass <? extends T> // compile error, cannot resolve T
這是因為 wildcard 只能用來定義參數。
所以我們最多只能限制 T 所繼承的型別:
class MyClass <T extends OtherClass> // ok
public interface List<E> extends Collection<E> // ok
因為 wildcard 的限制,Java 只能在建立物件時定義我們想要的變型,這個定義變型的時機我們稱為 使用點變型 (Use Site Variance)。
使用點變型 Use Site Variance
在 建立型別時才定義泛型的變型
Java 範例如下:
public interface List<E> extends Collection<E> {
<T> T[] toArray(T[] a);
//...
}List <? extends NormalDog> eDogs = new ArrayList<SuperDog>();
List <? super NormalDog> sDogs = new ArrayList<SuperDog>();
當然, Kotlin 也可以用 Use Site Variance 來定義變型,但在 Kotlin 中我們使用了 Type Projection。
Type Projection 基本上就是將 Java 變型中的 ? extends/super 濃縮成一個關鍵字 out/in 並取而代之。
class MutableList<T> {}val eDogs: MutableList<out NormalDog> = mutableListOf()
val sDogs: MutableList<in NormalDog> = mutableListOf()
雖然說 使用點變型 能讓程式更有彈性,但每次都要定義還是有點麻煩。如果我希望能建立一個預設就是某變型的類別我該如何做呢?
這時我們就要看看 宣告處變型 (Declaration Site Variance)。
宣告處變型 Declaration Site Variance
在設計型別時就設定變型
雖說 Java 的 wildcard 有所使用限制,但如果能用的話,那對我們有什麼好處呢?
Use Site Variance vs Declaration Site Variance
其實這兩種定義變型的方式各有千秋,也被不同的語言所使用。
譬如,Scala 與 C# 便使用了 DSV,而 Java 則選擇 USV。
USV 的優缺點
USV 與 DSV 相較之下更有可塑性,因為 USV 讓使用者隨時隨意地定義出自己需要的變型。
但是,這也表示大多數的泛型責任都是由使用者來承擔,所以複雜度也相對增加。
DSV 的優缺點
DSV 能幫使用者將部分的變型設計由套件設計師負責,因此減少使用上的複雜度。
但由於變型是在宣告時就被設定,所以相對的少了可塑性。
另外,根據 Altidor et al, 2011 [1], 若 Java 也能用 Declaration site,那麼 Java Library 中大約有 39% 的 Use site 可被取代。
由此可見,無論是 Use site 與 Declaration site 都各自有各自的優缺點。
而 Kotlin 就是為了滿足兩者 (mix site variance) 而設計出來的。
在定義 宣告處變型 時,Kotlin 也是使用 Type Projection :
public interface List<out E> : Collection<E> {}
這種設計,不僅僅讓我們可以簡單使用 List 也強行定義 List 為 逆變。因此 List 屬於 Read Only。
在了解這些變型的定義點後,你可好奇,Kotlin 中的 wildcard 又要如何定義呢?
Java wildcard
從上一篇得知,Java 的 wildcard 使用方式如下:
List<?> wildList = new ArrayList<Any Type>();List<? extends AnyType> coList
= new ArrayList<Type that extends from AnyType>List<? super AnyType> contraList
= new ArrayList<Type that extends from AnyType>
那麼 Kotlin 呢?
Kotlin 其實也有一個類似 wildcard 的關鍵字元:* (又稱作 Star Projection)
以下是 Star Projection 在 Kotlin 中的用法。
Kotlin Star Projection
val ls: List<*> = listOf<Any Type>()
因為 Star Projection 無法跟變型同時使用,因此這就是唯一的使用方法。
那 Star Projection 在不同的變型代表著什麼呢? 為此,我們先創立一個什麼變型都有的類別:
class KBigBox<T, out O, in I> {
private var _t: T? = null
private var _o: O? = null
private var _i: I? = null
fun getT(): T? = _t
fun getO(): O? = _o
fun setT(t: T) { _t = t }
fun setI(i: I) { _i = i }
}
然後我們宣告如下:
val kBigBox: KBigBox<*, *, *> = KBigBox<Int, Int, Int>()
當我們試著呼叫 get/set 時你會發現:
不變與協變都會回傳 Any? 。
首先,Any 其實就是 Java 中的 Object,但是沒有 null 值。
而 Any? 則是 Object 與 null 值了。
相對的,當呼叫 set 時:
不變與逆變都只能帶入 Nothing。
Nothing 比較特別,它表示著一個不存在的值。 所以當輸入型別為 Nothing 時,表示無法輸入。 相反,當方法回傳 Nothing 時,代表這方法不會回傳。
所以從上面得到的結論是:
KBigBox <*, out *, in *>讀取時:KBigBox <out Any?, out Any?, ---> // 逆變無法讀取寫入時: KBigBox <in Nothing, ---, in Nothing> // 協變無法輸入
因此,泛型與 Star Projection配合下,讀取會被當作協變處理,而寫入時則會以逆變處理。
另外,當我們類別參數有設 Upper Bound 或 Lower Bound 時:
class KBigBox<T: SmartDog, out O: SmartDog, in I: SmartDog>
協變 (out) 設的是 Upper Bound ; 逆變 (in) 則是 Lower Bound
這時:
KBigBox <*: SmartDog, out *: SmartDog, in *: SmartDog>讀取時:KBigBox <out SmartDog?, out SmartDog?, ---> // 逆變無法讀取寫入時: KBigBox <in Nothing, ---, in Nothing> // 協變無法輸入
所以,結合上下結果我們可以得出以下規則:
KBigBox <*: T, out *: T, in *: T>讀取時:KBigBox <out T, out T, ---> // 逆變無法讀取寫入時: KBigBox <in Nothing, ---, in Nothing> // 協變無法輸入
經由這些測試,雖然 Kotlin 官方是說 star projection 很像 Java 的 raw type,但是我倒是認為比較像是 wildcard。
現在你已經知道泛型可以用在型別 (類別、介面) 上,但好像都沒談過如何使用在方法上。
泛型方法
想在方法中定義泛型有兩種方式:
- 針對類別已知的泛型來設計
- 針對未知的泛型來設計
Java
public interface List<E> extends Collection<E> {
E get(int index); // 已知泛型
<T> T[] toArray(T[] a); // 為知泛型
}
當然,我們也可以定義出類別泛型方法:
class MyClass<T> {
static <U> void doSomethingWith(U u) {} // ok
static <T> void doSomethingWithT(T t) {} // failed
}
當中要注意的是 static 是無法取得類別的泛型的。
除此之外,我們也可以定出繼承上限:
class MyClass2<T> {
static <U extends Comparable<U>> void doSomethingWith(U u) {}
}
Kotlin
Kotlin 與 Java 的泛型定義很類似:
public interface List<out E> : Collection<E> {
public operator fun get(index: Int): E
// toArray (deprecated) 則是呼叫 Java 中的方法
}
由於 Kotlin 可以隨時隨地定義方法,包括 Extension。 所以我們還可以定義方法如下:
fun <T> getItem(list: List<T>, index: Int): T = list[index]
fun <T> T.getString(): String = this.toString()
當然,Kotlin 也可以加上繼承上限:
fun <T: Comparable<T>> T.getString(): String = this.toString()
結論
變型的定義可以發生在兩個地方:
- 使用點變型 (Use Site Variance)
- 宣告點變型 (Declaration Site Variance)
目前 Java 雖然只支援使用點變型,但是 USV 的優點在於它的靈活度與可塑性。
而 Kotlin 除了支援使用點變型 也支援 宣告處變型。DSV 的優點是將部分的變型的定義責任分給框架設計師,而不是全部都由使用者設計。
相較於 Java 的 wildcard,在 Mixed Site Variance 的情形下,Kotlin 的 Star Projection (*) 在不同的變型下的結果如下:
Foo<T: SomeClass>
Foo<?> 表示 Foo<? extends SomeClass>
Foo<*> 讀取時表示 Foo<out SomeClass> 寫入時表示 Foo<in Nothing>Foo<out T: SomeClass> (Java 無法使用 Declaration Site Variance)
Foo<*> 表示 Foo<out SomeClass?>Foo<in T: SomeClass>
Foo<*> 表示 Foo<in Nothing>
若沒有設 SomeClass → 則 Java 預設為 Object,Kotlin 預設為 Any
最後,除了能在型別 (類別、介面) 定義泛型和變型外,我們還可以在方法上定義泛型。而方法也分成兩種:類別方法 、實體方法
Java :
// 類別方法
class MyClass<T> {
static <U> void doSomethingWith(U u) {} // ok
static <T> void doSomethingWithT(T t) {} // failed
}// 實體方法
public interface List<E> extends Collection<E> {
E get(int index); // 已知泛型
<T> T[] toArray(T[] a); // 為知泛型
}
Kotlin :
// 類別方法
class MyInOutClass<in T, out T2> {
companion object {
fun <U> doSomethingWith(u: U) {
}
}// 實體方法
public interface List<out E> : Collection<E> {
public operator fun get(index: Int): E
}
Kotlin 除了在型別中定義外,也可以定義全域方法與 extension :
fun <T> getItem(list: List<T>, index: Int): T = list[index]
fun <T> T.getString(): String = this.toString()
有不明白的或是有寫錯的,歡迎留言指教。
如果這篇文章對您有幫助,那就請您分享和給我點掌聲吧 ~~
後序
看完基本泛型的作法時,你有沒有想過 Java 5 才出來的泛型是怎麼向下相容的呢?
那就請繼續看下去
Reference
[1] Altidor, J., Huang, S.S., & Smaragdakis, Y. (2011). Taming the wildcards: combining definition- and use-site variance. PLDI ‘11.