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. 聲明點(diǎn)變形與使用點(diǎn)型變

實化類型參數(shù)允許再運(yùn)行時的內(nèi)聯(lián)函數(shù)調(diào)用中引用作為類型實參的具體類型拭嫁;
聲明點(diǎn)變型說明一個帶類型參數(shù)的泛型類型氨肌,是否是另一個泛型類型的子或超類型捂寿;
使用點(diǎn)變型在具體使用一個泛型類型時,達(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語法有點(diǎn)奇怪常挚,使用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 使用點(diǎn)變型:在類型出現(xiàn)的地方

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

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

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

// Java 使用點(diǎn)變型
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也支持使用點(diǎn)變型, 直接對應(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
    }
}

// 使用點(diǎn)變型:給類型參數(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 使用點(diǎn)變型
    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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坛悉,隨后出現(xiàn)的幾起案子伐厌,更是在濱河造成了極大的恐慌,老刑警劉巖裸影,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挣轨,死亡現(xiàn)場離奇詭異,居然都是意外死亡轩猩,警方通過查閱死者的電腦和手機(jī)卷扮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門荡澎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晤锹,你說我怎么就攤上這事摩幔。” “怎么了鞭铆?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵或衡,是天一觀的道長。 經(jīng)常有香客問我车遂,道長封断,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任舶担,我火速辦了婚禮澄港,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘柄沮。我一直安慰自己,他們只是感情好废岂,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布祖搓。 她就那樣靜靜地躺著,像睡著了一般湖苞。 火紅的嫁衣襯著肌膚如雪拯欧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天财骨,我揣著相機(jī)與錄音镐作,去河邊找鬼。 笑死隆箩,一個胖子當(dāng)著我的面吹牛该贾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捌臊,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼杨蛋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了理澎?” 一聲冷哼從身側(cè)響起逞力,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糠爬,沒想到半個月后寇荧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡执隧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年揩抡,在試婚紗的時候發(fā)現(xiàn)自己被綠了户侥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡捅膘,死狀恐怖添祸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情寻仗,我是刑警寧澤刃泌,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站署尤,受9級特大地震影響耙替,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜曹体,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一俗扇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧箕别,春花似錦铜幽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至母截,卻和暖如春到忽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背清寇。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工喘漏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人华烟。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓翩迈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親盔夜。 傳聞我的和親對象是個殘疾皇子帽馋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容