Kotlin 之 泛型

參考:

  1. Kotlin 實戰(zhàn)
  2. Java 泛型推薦閱讀:https://www.zhihu.com/question/20400700

代碼與說明
Kotlin 分享系列,來自部門團(tuán)隊幾個伙伴一起整理,全部MD與代碼都在:
https://github.com/zhaoyubetter/KotlinShare
Kotlin 泛型基本上與Java泛型類型,了解了Java泛型,基本就了解了Kotlin的泛型

Kotlin 泛型內(nèi)容包括:

  1. 泛型函數(shù)與類
  2. 類型擦除與實化類型參數(shù)
  3. 聲明點變形與使用點型變

實化類型參數(shù)允許再運(yùn)行時的內(nèi)聯(lián)函數(shù)調(diào)用中引用作為類型實參的具體類型;
聲明點變型說明一個帶類型參數(shù)的泛型類型,是否是另一個泛型類型的子或超類型;
使用點變型在具體使用一個泛型類型時,達(dá)到與Java通配符一樣的效果;

1. 泛型類型參數(shù)

可以定義帶類型形參的類型,當(dāng)此類型的實例被創(chuàng)建時,類型形參被替換成類型實參的具體類型,如:List<String>, Map<K, V> 等;

在使用時,Kotlin 編譯器能推導(dǎo)出類型實參:

val list = listOf("ABC", “DEF”)  // 等價于 val list = listOf<String>("ABC", “DEF”) 

1.1 泛型函數(shù)和屬性

這個概念跟Java一樣,泛型函數(shù)有自己的類型形參,泛型函數(shù)使用時,在調(diào)用初,會被替換成具體的類型實參;
比如:

fun  <T> List<T>.slice(incides:IntRange):List<T>

泛型形參聲明 <T> 放在 fun 關(guān)鍵字之后,使用跟Java類似;

泛型的擴(kuò)展屬性

val <T> List<T>.penultimate: T
    get() = this[size - 1]

注意: 不能聲明泛型非擴(kuò)展屬性,不能在一個類的屬性中存儲多個不同類型的值;如要這么做,需考慮泛型類

1.3 泛型類、接口

與Java一樣,在類聲明時,可指定泛型,一旦聲明之后,就可以在類的主體中像其他類型一樣使用類型參數(shù)了;如:

interface List<T> {
    operator fun get(index: Int) : T // ....
}

如果類繼承自泛型類(接口),就得為基礎(chǔ)類型的泛型形參提供具體類型實參或者另外的類型形參
比如:

interface List<T>
class StringList : List<String> // 具體類型實參
class MyList<T> : List<T>       // 泛型類型形參

1.4 類型參數(shù)約束

約束用來說明,只能使用什么樣的類型實參;

上界約束:
在泛型類型具體的初始化中,其對應(yīng)的類型實參,必須是 具體類型,或者子類型;
如下:(Java 使用 <T extends Number>

fun <T: Number> List<T>.sum():T    // : Number 表示上界

// java
public  <T extends Number> void test(T t) {
}

注意:這里不涉及到 out、in 生產(chǎn)者,消費(fèi)者關(guān)系,在參數(shù)位置,才涉及到,他們在后面;

我們也可以指定多個約束,如同Java 中 (<T extends Number & Appendable>),但Kotlin語法有點奇怪,使用where

fun <T> List<T>.sum2():T where T : Number , T: Appendable {}

1.5 讓類型形參非空

沒有指定上界的類型形參將會使用 Any? 這個默認(rèn)上界;
如下:

class Processor<T> {
    fun process(value: T) {
        value?.hashCode()
    }
}

fun main(args: Array<String>) {
    // 可空類型String?被用來替換T
    val nullableString = Processor<String?>()
    // 可傳遞null
    nullableString.process(null)
}

如何不允許null呢,使用<T:Any>來確保類型T是非空類型;

class Processor<T : Any> {
    fun process(value: T) {
        value.hashCode()    // ?. 可以去掉了
    }
}

2. 運(yùn)行時的泛型:擦除和實化類型參數(shù)

Java中,泛型通過類型擦除實現(xiàn);
在Kotlin可通過聲明一個inline函數(shù)實現(xiàn)類型實參,不被擦除(Kotlin稱實化);

2.1 運(yùn)行時的泛型:類型檢查和轉(zhuǎn)換

跟Java類似,Kotlin中的泛型在運(yùn)行時也被擦除了;擦除是有好處的,這樣保存在內(nèi)存中類型信息就少了;

在Kotlin中,不允許使用沒有指定類型實參的泛型類型,如果想判斷一個變量是否是列表,可傳遞 * 星投影;

val list = listOf(1,2,3)
    if(list is List<*>) {  // 星投影,類似Java的 <?>
}

使用 as 、 as? 進(jìn)行轉(zhuǎn)換:

fun printTest(c: Collection<*>) {
    val intList = c as? kotlin.collections.List<Int> ?:
            throw IllegalArgumentException("轉(zhuǎn)換失敗")
    println(intList)
}

2.2 聲明帶實化類型參數(shù)的函數(shù)

因為泛型會被擦除,比如下面的代碼是報錯的:

fun <T> isA(value: Any) = value is T   // 不能確定T

但通過inline 內(nèi)聯(lián)函數(shù),會把每一個的函數(shù)調(diào)用換成實際的代碼調(diào)用,lambda 也是一樣,并結(jié)合 reified 標(biāo)記類型參數(shù),上面的 value is T 就可以通過編譯了

inline fun <reified T> isA(value: Any) = value is T
fun main(args: Array<String>) {
    println(isA<Int>(1))
}

為什么實化只對內(nèi)聯(lián)函數(shù)有效

在內(nèi)聯(lián)函數(shù)中 可以寫 value is T,普通類、普通函數(shù)中卻不可以;
因為編譯器將內(nèi)聯(lián)函數(shù)的字節(jié)碼直接添加調(diào)用處,當(dāng)每次調(diào)用帶實化類型參數(shù)的函數(shù)時,編譯器知道了類型實參的確切類型;而kotlin 中調(diào)用,不能省略類型實參, 上面的 不能寫成 isA(1),編譯直接報錯
注意reified的內(nèi)聯(lián)函數(shù)不能再Java中調(diào)用,普通的 inline 函數(shù)可以;

3. 變型:泛型和子類型化

變型描述了擁有相同基礎(chǔ)類型不同類型實參(泛型)類型之間是如何關(guān)聯(lián)的:如,List<String> 與 List<Any>;
變型的意義在于設(shè)計的API,不會以不方便的方式限制用戶,也不會破壞用戶所期望的類型安全;

3.1 為什么存在變型: 給函數(shù)傳遞實參

把一個 List<String 類型的變量傳給 List<Any> 這樣是允許的,如:

fun printContents(c: List<Any>) {
    println(c.joinToString(""))
}
fun main(args: Array<String>) {
    printContents(listOf("a","b"))
}

但是下面的代碼

fun addContent(list: MutableList<Any>) {
    list.add(1234)
}
// 下面的代碼調(diào)用,明顯有問題
val list = mutableListOf<String>("cccc")
addContent(list)    // 編譯通不過

3.2 類、類型與子類型

變量的類型,規(guī)定了該變量的可能值,類型和類不是相同的概念;

非泛型類 類的名稱可以直接當(dāng)做類型使用;如:

var s : String 
var s : String?

每個Kotlin的都可以用于構(gòu)造至少2種類型;

泛型類 情況復(fù)雜,要得到一個合法的類型,需要用類型實參替換(泛型)類的類型形參
如:List不是一個類型(它是一個類),List<Int>、List<String?>是合法的類型;

子類型、超類型用來描述類型之間的關(guān)系,

如果需要類型A的值,都能夠使用類型B的值(當(dāng)做A的值),則類型B就稱為類型A的子類型;超類型反之;
比如:Number 是 Int 的超類型,Int 是Number 的子類型;

這樣的情況下,子類型子類本質(zhì)上是同一回事;
當(dāng)涉及到泛型類型時,子類型與子類就有差異了;List<String> 是 List<Any> 的子類型嗎?對于只讀List接口,是的,而:MutableList<String> 當(dāng)做Mutable<Any>的子類型是不安全的;

一個泛型類 - 如:MutableList 如果任意2種類型A和B,MutableList<A> 既不是MutableList<B>的子類型,也不是它的超類型,稱為在該類型參數(shù)上是不變型的;

對于List,Kotlin 中的List接口表示的是只讀集合,如果A是B的子類型,那List<A> 是 List<B> 的子類型,這樣的類or接口被稱為協(xié)變

3.3 協(xié)變:保留子類型化關(guān)系

協(xié)變說明子類型化被保留了, 在Kotlin中,要聲明類在某個類型參數(shù)上是協(xié)變的,在該類型參數(shù)的名稱上添加 out 關(guān)鍵字;

interface Producter<out T> {
    fun produce() : T
}

有什么用?看例子

open class Animal {   
    fun feed() {
    }
}

// 泛型類,接收Animal子類
class Herd<T : Animal> {
    val size: Int get() = 20
    operator fun get(i:Int) : T { ... }   // 操作符重載
}
// 具體動物
class Cat : Animal() {
    fun clean() {}
}
// 喂方法,不好意思,我只認(rèn) Animal,不然他的子類
fun feedAll(animals : Herd<Animal>) {
    for(i in 0 until animals.size) {
        animals[i].feed()
    }
}

fun takeCareOfCats(cats: Herd<Cat>) {
   feedAll(cats)   // 期望 Herd<Animal>,很遺憾報錯了    
}

怎么辦?使用out關(guān)鍵字,改成協(xié)變

// 泛型類
class Herd<out T : Animal> {    

注意:這里的<out T: Animal> 與上面的提高的1.4 類型參數(shù)約束是不一樣的,
類型約束,<> 在 fun 之后,這里是在方法 or 類的后面;

out 位置,表示這個類只能生產(chǎn)類型T的值,而不能消費(fèi)他們;

在類成員的聲明中類型參數(shù)的使用可分為in 位置out位置

interface MyTranform<T> {
    fun tranform(t: T): T   // 參數(shù) t,in 位置,返回值 out位置
}

類的類型參數(shù)前的out 、in關(guān)鍵字約束了使用T的可能性,保證了對應(yīng)子類型關(guān)系的安全性;

Out 關(guān)鍵字的2個含義

  • 子類型化會被保留(Producer<Cat>) 是(Producer<Animal>)的子類型;
  • T 只能用在out位置(生產(chǎn)位置)

Kotlin 中的 List接口

// out 位置
public interface List<out E> : Collection<E> {...}

// T 在 in out 位置
public interface MutableList<E> : List<E>, MutableCollection<E> {...}

3.4 逆變:反轉(zhuǎn)子類型化關(guān)系

逆變是協(xié)變的鏡像:對于逆變類,它的子類型化關(guān)系與用作類型實參的類的子類型化關(guān)系是相反的;

// in 位置,表示消費(fèi)
val anyComparator = Comparator<Any> {
    e1, e2 ->
    e1.hashCode() - e2.hashCode()
}
fun main(args: Array<String>) {
    val strings = listOf("B", "A", "War")
    println(strings.sortedWith(anyComparator))
}

如需在特定類型的對象比較,可使用能處理該類型或它的超類型的比較器;
Comparator<Any> 是 Comparator<String>的子類型,其中Any是String的超類型;不同類型之間的子類型關(guān)系 與 這些類型的比較器間的子類型化關(guān)系是相反的;

逆變 如果B是A的子類型,那么Consumer<A> 就是Consumer<B>的子類型,類型參數(shù)A與B交換了位置;協(xié)變:子類型化關(guān)系復(fù)制了它的類型實參的子類型化關(guān)系,逆變則反過來

in 關(guān)鍵字:對應(yīng)類型的值是傳遞進(jìn)來給這個類的方法,并且被方法消費(fèi);

協(xié)變 逆變 不變
Producer<T> Consumer<in T> MutableList<T>
類的子類型化保留:Producer<Cat> 是 Producer<Animal> 的子類型 子類型反轉(zhuǎn):Consumer<Animal> 是 Consumer<Cat>的子類型 沒有子類型化
T只能在out位置 T只能在in位置 任何位置

表格:協(xié)變、逆變和不變

協(xié)變 逆變 不變
Producer<T> Consumer<in T> MutableList<T>
類的子類型化保留:Producer<Cat> 是 Producer<Animal> 的子類型 子類型反轉(zhuǎn):Consumer<Animal> 是 Consumer<Cat>的子類型 沒有子類型化
T只能在out位置 T只能在in位置 任何位置

類可以在一個類型參數(shù)上協(xié)變,另一個參數(shù)上逆變,比如Function接口;

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

3.5 使用點變型:在類型出現(xiàn)的地方

聲明點變型:在類聲明的時候,指定變型修飾符,這些修飾符會應(yīng)用到所有類被使用的地方;
使用點變型:在Java中,使用(super, extends)通配符,處理變型,使用帶類型參數(shù)時,指定是否可用這個類型參數(shù)的子或者超類替換;

Kotlin 聲明點變型 vs Java 通配符
聲明點變型更加簡潔,指定一次變型修飾符,所有這個類的使用者,都會添加約束;Java 中使用:Function(? super T, ? extends R) 來創(chuàng)建約束;

如下代碼片段(發(fā)現(xiàn)了區(qū)別,聲明處變型是不是更簡潔呢?):

// Java 使用點變型
public interface Stream<T> {
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

// Kotlin 聲明處變型
public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

Kotlin也支持使用點變型, 直接對應(yīng)Java的限界通配符;

// (不變型) 從一個集合copy到另一個集合
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
    for(item in source) {
        destination += item
    }
}

// 特定類型
fun <T : R, R> copyData2(source: MutableList<T>, destination: MutableList<R>) {
    for (item in source) {
        destination += item
    }
}

// 使用點變型:給類型參數(shù)加上 變型修飾符 (out 投影)
fun <T> copyData3(source: MutableList<out T>, destination: MutableList<T>) {
    for (item in source) {
        destination += item
    }
}

// 測試函數(shù)
fun main(args: Array<String>) {
    // copyData 不變型
    val source = mutableListOf("abc", "efg")
    val destination = mutableListOf<String>()
    copyData(source, destination)

    // copyData2 
    val source2 = mutableListOf("abc", "efg")
    var destination2 = mutableListOf<Any>()
    copyData2(source2, destination2)

    // copyData3 使用點變型
    val source3 = mutableListOf("better", "cc")
    var destination3 = mutableListOf<Any>()
    copyData3(source3, destination3)
    println(destination3)
}

類型投影
可以為類型聲明中類型參數(shù)指定變型修飾符,包括:形參類型(方法上的),局部變量類型、函數(shù)返回類型等,這稱作類型投影;投影即受限
如上copyData3函數(shù)的 source不是一個常規(guī)的的MutableList,而是一個投影(受限)的MutableList。只能調(diào)用返回類型是泛型類型參數(shù)的那些方法,也就是out位置的方法;

3.6 * 星號投影

星號投影,用來表示不知道關(guān)于泛型實參的任何信息,跟Java的?問號通配符類似;
注意
MutableList<*> 和 MutableList<Any?> 不一樣;
MutableList<T> 在T上是不變型的,

  • MutableList<Any?>包含的是任何類型的元素;
  • MutableList<*>是包含某種特定類型元素,但不知是哪個類型,所以不能寫入;但可讀??;
val list: MutableList<Any?> = mutableListOf('x', 1, "efg")
list.add(5)
val chars = mutableListOf('a', 'b', 'c')

val unkownElems: MutableList<*> = if (Random().nextBoolean()) list else chars
// unkownElems.add(12)  // 不能調(diào)用
println(unkownElems.get(0))

上例中,編譯器將MutableList<*> 當(dāng)做out投影的類型 MutableList<out Any?>,不能讓她消費(fèi)任何東西;

用處:
當(dāng)類型實參的信息并不重要時,可使用星號投影的語法:

  • 不需要使用任何在簽名中引用類型參數(shù)的方法;
  • 只是讀取數(shù)據(jù)而不關(guān)系它的具體類型;
fun <T> getFirst(list: List<*>): T? {    // 星號投影
    if (!list.isEmpty()) {
        return list.first() as T
    }
    return null
}
fun <T> getFirst2(list: List<T>): T {
    return list.first()
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容