Kotlin 泛型
聲明泛型類
和Java一樣,Kotlin通過在類名稱后面加上一對尖括號,并把類型參數(shù)放在尖括號內(nèi)來聲明泛型類和泛型接口。
interface List<T> {
operator fun get(index: Int): T
}
聲明泛型方法
fun <T> sum(t: T) {
//do something
}
類型參數(shù)約束
上界約束
如果你把一個類型指定為泛型類型形參的上界約束,在泛型類型具體的初始化中,其對應(yīng)的類型實(shí)參就必須是這個具體類型或者它的子類型。
在Kotlin中聲明帶泛型上限的類和方法
class Base<T:Number> {
}
fun <T : Number> test(t: T) {
//do something
}
在Java中聲明帶泛型上限的類和方法
class Base<T extends Number> {
}
public <T extends Number> void test(T num) {
//do something
}
傳遞T的類型實(shí)參必須是Number或者Number的子類。
為類型參數(shù)指定多個約束的語法
Kotlin的語法
//泛型類
class Base<T> where T : CharSequence, T : Appendable {
}
//泛型方法
fun <T> test(num: T) where T : CharSequence, T : Appendable {
//do something
}
Java的語法
//泛型類
class Base<T extends CharSequence & Appendable> {
}
//泛型方法
public <T extends CharSequence & Appendable> void test(T num) {
}
聲明帶實(shí)化類型參數(shù)的函數(shù)
注意:實(shí)化類型參數(shù)只能用在內(nèi)聯(lián)函數(shù)上。
看一個例子
fun <T> isA(value: Any): Boolean {
//編譯錯誤,無法進(jìn)行類型檢查,只能看做Any類型
//return value is T
}
由于泛型的類型擦除,在運(yùn)行的時候無法知道傳入的類型實(shí)參具體是什么。所以無法進(jìn)行類型檢查,只能看做Any類型。
內(nèi)聯(lián)函數(shù)的類型形參能夠被實(shí)化,意味著你可以在運(yùn)行時引用實(shí)際的類型實(shí)參。
inline fun <reified T> isAReified(value: Any): Boolean {
return value is T
}
reified聲明了類型參數(shù)不會在運(yùn)行時被擦除(實(shí)化類型參數(shù))。
使用
println(isAReified<String>("abc"))//true
println(isAReified<String>(123))//false
實(shí)化類型參數(shù)一個有意義的例子。下面是Kotlin庫函數(shù) filterIsInstance 方法的簡化版本。
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
val destination = ArrayList<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
使用
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
通過指定String作為函數(shù)的類型實(shí)參,表明只對字符串感興趣。因此函數(shù)的返回類型是 List<String>。在這種情況下,類型實(shí)參在運(yùn)行時是已知的,函數(shù) filterIsInstance 可以檢查列表中的值是不是類型實(shí)參類型的實(shí)例。
為什么實(shí)化只對內(nèi)聯(lián)函數(shù)有效?因?yàn)榫幾g器把實(shí)現(xiàn)內(nèi)聯(lián)函數(shù)的字節(jié)碼插入到每一次調(diào)用發(fā)生的地方。生成的字節(jié)碼引用了具體類,而不是類型參數(shù),它不會被運(yùn)行時發(fā)生的類型擦除影響。
注意:帶reified類型參數(shù)的內(nèi)聯(lián)函數(shù)不能在Java代碼中調(diào)用。
看一個在Android中的例子,使用實(shí)化類型參數(shù)替代類引用。
在Java中定義一個通用的startActivity的方法
public <T extends Activity> void startActivity(Class<T> clazz) {
Intent intent = new Intent(this, clazz);
startActivity(intent);
}
調(diào)用需要傳遞Class對象。
startActivity(MainActivity.class);
Kotlin使用實(shí)化類型參數(shù)替代類引用
//給Context定義一個擴(kuò)展函數(shù)
inline fun <reified T : Activity> Context.startActivity() {
//內(nèi)聯(lián)函數(shù)中的實(shí)化類型參數(shù),可以引用到具體的類
val intent = Intent(this, T::class.java)
startActivity(intent)
}
調(diào)用直接傳遞類名即可
startAct<MainActivity>()
泛型的變型
變型的概念描述了擁有相同基礎(chǔ)類型和不同類型實(shí)參(泛型)類型之間是如何關(guān)聯(lián)的:例如List<String>和List<Any>之間是如何關(guān)聯(lián)。
先看一個例子
假設(shè)Orange類是Fruit類的子類,Crate<T> 是一個泛型類,那么,Crate<Orange> 是 Crate<Fruit> 的子類型嗎?直覺可能告訴你,Yes。但是,答案是No。
對于Java而言,兩者沒有關(guān)系。對于Kotlin而言,Crate<Orange> 可能是Crate<Fruit> 的子類型,或者其超類型,或者兩者沒有關(guān)系,這取決于Crate<T> 中的 T 在類 Crate 中是如何使用的。
- invariance(不變型):也就是說,
Crate<Orange>和Crate<Fruit>之間沒有關(guān)系。 - covariance(協(xié)變):也就是說,
Crate<Orange>是Crate<Fruit>的子類型。 - contravariance(逆變):也就是說,
Crate<Fruit>是Crate<Orange>的子類型。
不變型
一個泛型類一一例如 MutableList 如果對于任意兩種類型 A,B,MutableList<A>既不是 MutableList<B> 的子類型也不是它的超類型,它被稱為在該類型參數(shù)上是不變型的。 Java中所有的類都是不變型的(盡管那些類具體的使用可以標(biāo)記成可變型的,稍后你就會看到)。
協(xié)變:保留子類型化關(guān)系
一個協(xié)變類是一個泛型類(我們以Producer<T>為例),對這種類來說,下面的描述是成立的:如果A是B的子類型,那么Producer<A> 就是 Producer<B> 的子類型。我們說,子類型被保留了。例如,Producer<Cat> 是 Producer<Animal> 的子類型,因?yàn)?Cat 是 Animal 的子類型。
在Kotlin中要聲明類在某個類型參數(shù)上是可以協(xié)變的,在該類型參數(shù)的名稱前面加上out關(guān)鍵字即可。
interface Producer<out T> {
fun produce(): T
}
看一個例子
//open關(guān)鍵字,讓類可以被繼承
open class Animal {
fun fead() {
println("喂養(yǎng)小動物")
}
}
class Cat : Animal() {
fun cleanLitter() {
}
}
class Herd<out T : Animal> {
val list = listOf<T>()
val size: Int get() = list.size
operator fun get(i: Int): T {
return list[i]
}
}
fun feedAll(animals: Herd<Animal>) {
for (i in 0 until animals.size) {
animals[i].fead()
}
}
使用
fun takeCatOfCats(cats: Herd<Cat>) {
for (i in 0 until cats.size) {
cats[i].cleanLitter()
}
//注釋1處
feedAll(cats)
}
注釋1處,不需要類型轉(zhuǎn)換,可以看到通過協(xié)變保留了 Animal 和 Cat 之間的子類型關(guān)系。Herd<Cat> 是 Herd<Animal> 的子類。
注意:你不能把任何類都變成協(xié)變的:這樣不安全。讓類在某個類型參數(shù)變?yōu)閰f(xié)變,限制了該類中對該類型參數(shù)使用的可能性。要保證類型安全,它只能用在所謂的 out 位置,意味著這個類只能生產(chǎn)類型 T 的值而不能消費(fèi)它們。
在類成員的聲明中類型參數(shù)的使用可以分為 in 位置和 out 位置??紤]這樣一個類,它聲明了一個類型參數(shù) T 并包含了一個使用 T 的函數(shù)。如果函數(shù)是把 T 當(dāng)成返回類型,我們說它在 out 位置。這種情況下,該函數(shù)生產(chǎn)類型為 T 的值。如果 T 用作函數(shù)參數(shù)的類型,它就在 in 位置。這樣的函數(shù)消費(fèi)類型為 T 的值,如下圖所示:

對于Kotlin而言,可以這么說: Producer out,Consumer in。
類的類型參數(shù)前的 out 關(guān)鍵字要求所有使用 T 的方法只能把 T 放在 out 位置而不能放在 in 位置。這個關(guān)鍵宇約束了使用 T 的可能性,這保證了對應(yīng)子類型關(guān)系的安全性。
重申一下類型參數(shù)T上的關(guān)鍵字 out 有兩層含義:
- 子類型化被保留(
Producer<Cat>是Producer<Animal>的子類型) - T 只能用在out的位置
Java中實(shí)現(xiàn)協(xié)變的方式:上限通配符。
class Fruit {
}
class Orange extends Fruit {
}
public static void main(String[] args) {
//編譯錯誤
//List<Fruit> fruits = new ArrayList<Orange>();
List<? extends Fruit> fruits = new ArrayList<Orange>();
//編譯錯誤:不能添加任何類型的對象
//fruits.add(new Orange());
//fruits.add(new Fruit());
//fruits.add(new Object());
//fruits.add(null);//可以這么做,但是沒有意義
//我們知道,返回值肯定是Fruit
Fruit f = fruits.get(0);
}
我們之所以可以安全地將
ArrayList<Orange>向上轉(zhuǎn)型為List<? extends Fruit>,是因?yàn)榫幾g器限制了我們對于List<? extends Fruit>類型部分方法的調(diào)用。例如void add(T t)方法,以及一切參數(shù)中含有 T 的方法(稱為消費(fèi)者方法),因?yàn)檫@些方法可能會破壞類型安全,只要限制這些方法的調(diào)用,就可以安全地將ArrayList<Orange>轉(zhuǎn)型為List<? extends Fruit>,這就是所謂的協(xié)變,通過限制對于消費(fèi)者方法的調(diào)用,使得像List<? extends Fruit>這樣的類型成為單純的“生產(chǎn)者”,以保證運(yùn)行時的類型安全。
逆變:翻轉(zhuǎn)子類型化關(guān)系
逆變的概念可以看成是協(xié)變關(guān)系的鏡像:對一個逆變類來說,它的子類型化關(guān)系與用作類型實(shí)參的類的子類型化關(guān)系是相反的。
一個在類型參數(shù)上逆變的類是這樣一個泛型類(我們以Consumer<T>為例),對這種類來說,下面的描述成立:如果B是A的子類型,那么 Consumer<A> 就是Consumer<B>的子類型。例如,Cat 是 Animal 的子類型,但是Consumer<Animal> 卻成了 Consumer<Cat> 的子類型。
我們從Comparator接口的例子開始,該接口定義了一個compare方法,用來比較給定的兩個對象。
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int
}
如你所見,compare方法只是消費(fèi)類型為T的值。這說明T值在in位置使用,因此它的聲明之前用了in關(guān)鍵字。
一個為特定類型的值定義的比較器顯然可以用來比較該類型任意子類型的值。例如,如果有一個Comparator<Any>,可以用它比較任意具體類型。
舉個例子
val anyComparator: Comparator<Any> = Comparator<Any> { o1, o2 ->
o1.hashCode() - o2.hashCode()
}
val list: List<String> = listOf()
//期望傳入一個Comparator<String>
list.sortedWith(anyComparator)
public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> {
}
List<String>調(diào)用sortedWith方法期望傳入一個Comparator<String>, 傳給它一個能比較更一般類型的比較器是安全的。如果你要在特定類型的對象上執(zhí)行比較,可以使用能處理該類型或者它的超類型的比較器。這說明 Comparator<Any> 是 Comparator<String> 的子類型,其中Any是String的超類型。不同類型之間的子類型關(guān)系和這些類型的比較器之間的子類型化關(guān)系截然相反。
Java中實(shí)現(xiàn)逆變的方式:下限通配符。
public static void main(String[] args) {
List<Object> objs = new ArrayList<>();
objs.add(new Object());
List<? super Fruit> canContainFruits = objs;
//沒有問題,可以寫入Fruit類及其子類
canContainFruits.add(new Orange());
canContainFruits.add(new Fruit());
//無法安全地讀取,canContainFruits完全可能包含F(xiàn)ruit基類的對象,比如這里的Object
//Fruit f = canContainFruits.get(0);
//總是可以讀取為Object,然而這并沒有太多意義
Object o = canContainFruits.get(0);
}
編譯器限制了我們對于
List<? super Fruit>類型部分方法的調(diào)用。例如T get(int pos)方法,以及一切返回類型為 T 的方法(稱為生產(chǎn)者方法),因?yàn)槲覀儾荒艽_定這些方法的返回類型,只要限制這些方法的調(diào)用,就可以安全地將ArrayList<Object>轉(zhuǎn)型為List<? super Fruit>。這就是所謂的逆變,通過限制對于生產(chǎn)者方法的調(diào)用,使得像List<? super Fruit>這樣的類型成為單純的“消費(fèi)者”。
小結(jié):
- Kotlin中通過類型形參前面的
in和out關(guān)鍵字實(shí)現(xiàn)協(xié)變和逆變。 - Java中通過
<? extends T>上限的通配符和<? super T>下限通配符實(shí)現(xiàn)協(xié)變和逆變。
使用點(diǎn)變型:在類型出現(xiàn)的地方指定變型
先說一下聲明點(diǎn)變型:在類聲明的時候指定變型修飾符。這些修飾符會應(yīng)用到所有類被使用的地方。在聲明時一次性指定變型讓代碼變得簡潔和優(yōu)雅的多。
Kotlin中聲明點(diǎn)變型
public interface List<out E> : Collection<E> {
}
List接口中將類型參數(shù) E 聲明為是協(xié)變的。
聲明點(diǎn)變型帶來了更簡潔的代碼,因?yàn)橹挥弥付ㄒ淮巫冃托揎椃?,所有個類的使用者都不用再考慮這些了。
Java中所有的類都是不變型的。
//編譯錯誤,在Java中無法這樣聲明泛型類
class MyList< ? extends T> {
}
//編譯錯誤,在Java中無法這樣聲明泛型類
class MyList< ? super T> {
}
使用點(diǎn)變型:在每一次使用帶類型參數(shù)的類型的時候指定這個類型參數(shù)是否可以用它的子類型或者超類型替換。
Koltin中使用點(diǎn)變型的例子。
/**
* @param source 來源集合
* @param destination 目標(biāo)集合
* 來源的元素類型是目標(biāo)元素類型的子類型
*/
fun <T : R, R> copyData1(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
val ints = mutableListOf(1, 2, 3)
val anyItems = mutableListOf<Any>()
copyData1(ints, anyItems)
在這個例子中,來源集合只是讀取,目標(biāo)集合只是寫入。在這種情況下集合元素的類型不需要精確匹配。例如,把一個字符串的集合拷貝到可以包含任意對象的集合中一點(diǎn)問題也沒有。
但是Kotlin提供了一種更優(yōu)雅的表達(dá)方式。當(dāng)函數(shù)的實(shí)現(xiàn)調(diào)用了那些類型參數(shù)只出現(xiàn)在out位置(或只出現(xiàn)在in位置)的方法時,可以充分利用這一點(diǎn),在函數(shù)定義中給特定用途的類型參數(shù)加上變型修飾符。
使用變型修飾符修改上面的例子。
代碼清單 9.16
/**
* MutableList<out T> 和Java中的MutableList<? extends T> 是一個意思
*/
fun <T> copyData2(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination.add(item)
}
}
因?yàn)閬碓醇现皇亲x取(生產(chǎn))元素,即類型參數(shù)只出現(xiàn)在 out 位置,可以給類型參數(shù)加上 out 關(guān)鍵字。
/**
* MutableList<in T> 對應(yīng)到 Java的 MutableList<? super T>
*/
fun <T> copyData3(source: MutableList<T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
因?yàn)槟繕?biāo)集合只是添加(消費(fèi))元素,即類型參數(shù)只出現(xiàn)在 in 位置,可以給類型參數(shù)加上 in 關(guān)鍵字。
可以為類型聲明中類型參數(shù) 任意的用法指定變型修飾符,這些用法包括:形參類型(就像代碼清單 9.16 這樣)、局部變量類型、函數(shù)返回類型 等等。這里發(fā)生的一切被稱作類型投影(受限)。
類型投影(受限)
在代碼清單 9.16中,我們說source不是一個常規(guī)的MutableList,而是個投影(受限)的 MutableList,只能調(diào)用返回類型是泛型類型參數(shù)的那些方法,或者嚴(yán)格地講,只在out位置使用它的方法。編譯器禁止調(diào)用使用類型參數(shù)做實(shí)參(類型)的那些方法(在in位置使用類型參數(shù))。
看一個類型投影的例子
val list: MutableList<out Number> = mutableListOf()
//編譯錯誤,無法向MutableList添加數(shù)據(jù)
list.add(3)
在上面的代碼中,我們聲明了一個MutableList<Number>對象,但是發(fā)現(xiàn)我們無法調(diào)用MutableList的fun add(element: E)方法了。因?yàn)槲覀冇?code>out來指定類型參數(shù)Number是協(xié)變的。這時候我們的MutableList是一個類型投影(受限)的MutableList。編譯器禁止調(diào)用使用類型參數(shù)做實(shí)參(類型)的那些方法(在in位置使用類型參數(shù))來保證類型安全。
星號投影:使用*替代類型參數(shù)
在Kotlin中MyType<*> 對應(yīng)于Java的Mytype<?>,表示我們不關(guān)心關(guān)于泛型實(shí)參的任何信息。對于MutableList<*>這樣一個列表,因?yàn)槟悴恢朗悄膫€類型,所以無法向其中寫入任何東西。但是讀取是可行的,因?yàn)榱斜碇械闹刀寄芷ヅ銴otlin所有類型的超類型Any?。
參考鏈接:
- 《Kotlin實(shí)戰(zhàn)》
- Java和Kotlin中泛型的協(xié)變、逆變和不變
- Java 泛型,你了解類型擦除嗎?