Kotlin 知識梳理系列文章
Kotlin 知識梳理(1) - Kotlin 基礎(chǔ)
Kotlin 知識梳理(2) - 函數(shù)的定義與調(diào)用
Kotlin 知識梳理(3) - 類、對象和接口
Kotlin 知識梳理(4) - 數(shù)據(jù)類、類委托 及 object 關(guān)鍵字
Kotlin 知識梳理(5) - lambda 表達(dá)式和成員引用
Kotlin 知識梳理(6) - Kotlin 的可空性
Kotlin 知識梳理(7) - Kotlin 的類型系統(tǒng)
Kotlin 知識梳理(8) - 運(yùn)算符重載及其他約定
Kotlin 知識梳理(9) - 委托屬性
Kotlin 知識梳理(10) - 高階函數(shù):Lambda 作為形參或返回值
Kotlin 知識梳理(11) - 內(nèi)聯(lián)函數(shù)
Kotlin 知識梳理(12) - 泛型類型參數(shù)
一铲掐、本文概要
本文是對<<Kotlin in Action>>
的學(xué)習(xí)筆記,如果需要運(yùn)行相應(yīng)的代碼可以訪問在線環(huán)境 try.kotlinlang.org密强,這部分的思維導(dǎo)圖為:
二短蜕、運(yùn)行時的泛型:擦除和實化類型參數(shù)
2.1 運(yùn)行時的泛型
和
Java
一樣凶伙,Kotlin
的泛型在運(yùn)行時也被擦除了,這意味著 泛型類實例不會攜帶用于創(chuàng)建它的類型實參的信息冲秽。
例如舍咖,如果你創(chuàng)建了一個List<String>
矩父,在運(yùn)行時你只能看到它是一個List
锉桑,不能識別出列表本打算包含的是String
類型的元素。
接下來我們談?wù)劙殡S著擦除類型信息的約束窍株,因為類型實參String
沒有被存儲下來民轴,你不能檢查它們。例如球订,你不能判斷一個列表是一個包含字符串的列表還是包含其它對象的列表后裸,也就是說,在is
檢查中不可能使用類型實參中的類型冒滩,例如
fun main(args: Array<String>) {
val authors = listOf("first", "second")
if (authors is List<Int>) {}
}
將會在編譯時拋出下面的異常:
>> Cannot check for instance of erased type
Kotlin
不允許使用 沒有指定類型實參的泛型類型微驶,如果希望檢查一個值是否是列表,而不是set
或者其它對象开睡,可以使用特殊的 星號投影 語法來做這個檢查:
if (value is List<*>)
實際上因苹,泛型類型擁有的每個類型形參都需要一個*
,現(xiàn)在你可以認(rèn)為它就是 擁有未知類型實參的泛型類型篇恒。
在as
和as?
轉(zhuǎn)換中仍然可以使用一般的泛型類型扶檐,但是如果該類 有正確的基礎(chǔ)類型但類型實參是錯誤的,轉(zhuǎn)換也不會失敗胁艰,因為在運(yùn)行時轉(zhuǎn)換發(fā)生的時候類型實參是未知的款筑。因此,這樣的轉(zhuǎn)換會導(dǎo)致編譯器發(fā)出unchecked cast
的警告腾么,例如下面這段程序:
fun printSum(c: Collection<*>) {
val intList = c as? List<Int>
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
fun main(args: Array<String>) {
//(1) 正常運(yùn)行奈梳。
printSum(listOf(1, 2, 3))
//(2) as 檢查是成功的,但是調(diào)用 intList.sum() 方法時會拋出異常解虱。
printSum(listOf("a", "b", "c"))
}
在(2)
調(diào)用時攘须,并不會拋出IllegalArgumentException
異常,而是在調(diào)用sum
函數(shù)時才發(fā)生饭寺,因為sum
函數(shù)試著從列表中讀取Number
值然后把它們加在一起阻课,把String
當(dāng)做Number
使用的嘗試會導(dǎo)致運(yùn)行時的ClassCastException
。
假如在編譯期艰匙,Kotlin
已經(jīng)知道了相應(yīng)的類型信息限煞,那么is
檢查是允許的:
fun printSum(c: Collection<Int>) {
if (c is List<Int>) {
println(c.sum())
}
}
fun main(args: Array<String>) {
printSum(listOf(1, 2, 3))
}
c
是否擁有類型List<Int>
的檢查是可行的,因為我們將函數(shù)類型的形參類型聲明為了Collection<Int>
员凝,因此編譯期就確定了集合包含的是整型數(shù)字署驻。
不過,Kotlin
有特殊的語法結(jié)構(gòu)可以允許你 在函數(shù)體中使用具體的類型實參,但只有inline
函數(shù)可以旺上,接下來讓我們來看看這個特性瓶蚂。
2.2 聲明帶實化類型參數(shù)的函數(shù)
Kotlin
泛型在運(yùn)行時會被擦除,這意味著如果你有一個泛型類的實例宣吱,你無法弄清楚在這個實例創(chuàng)建時用的究竟是哪些類型實參窃这。泛型函數(shù)的實參類型也是這樣,在調(diào)用泛型函數(shù)的時候征候,在函數(shù)體中你不能決定調(diào)用它用的類型實參杭攻。
//將會在編譯時拋出 "Cannot check for instance of erased type : T" 的異常
fun <T> isA(value : Any) = value is T
內(nèi)聯(lián)函數(shù)的類型形參能夠被實化
只有一種例外可以避免這種限制:內(nèi)聯(lián)函數(shù)。內(nèi)聯(lián)函數(shù)的類型形參能夠被實化疤坝,意味著你可以 在運(yùn)行時引用實際的類型實參兆解。前面我們介紹過內(nèi)聯(lián)函數(shù)的兩個優(yōu)點(diǎn):
- 編譯器會把每一次函數(shù)調(diào)用都替換成函數(shù)實際的代碼實現(xiàn)
- 如果該函數(shù)使用了
lambda
,lambda
的代碼也會內(nèi)聯(lián)跑揉,所以不會創(chuàng)建匿名類
這里锅睛,我們介紹它一個新的優(yōu)點(diǎn):對于泛型函數(shù)來說,它們的類型參數(shù)可以被實化历谍。我們將方面的函數(shù)修改如下现拒,聲明為inline
并且用reified
標(biāo)記類型參數(shù),就能用該函數(shù)檢查value
是不是T
的實例:
inline fun <reified T> isA(value: Any) = value is T
fun main(args: Array<String>) {
println(isA<String>("abc"))
println(isA<String>(123))
}
運(yùn)行結(jié)果為:
>> true
>> false
filterIsIntance
函數(shù)可以接收一個集合扮饶,選擇其中那些指定類的實例具练,然后返回這些被選中的實例:
fun main(args: Array<String>) {
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
}
運(yùn)行結(jié)果為:
[one, three]
該函數(shù)的簡化實現(xiàn)為:
inline fun <reified T> Iterable<*>.filterIsIntance() : List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
}
為什么實化只對內(nèi)聯(lián)函數(shù)有效
我們之所以可以在inline
函數(shù)中使用element is T
這樣的判斷,而不能在普通的類或函數(shù)中執(zhí)行的原因是因為:編譯器把 實現(xiàn)內(nèi)聯(lián)函數(shù)的字節(jié)碼 插入每一次調(diào)用發(fā)生的地方甜无,每次你 調(diào)用帶實化類型參數(shù)的函數(shù) 時扛点,編譯器都知道這次特定調(diào)用中 用作類型實參的確切類型,因此岂丘,編譯器可以生成 引用作為類型實參的具體類 的字節(jié)碼陵究。
因為生成的字節(jié)碼引用了具體類,而不是類型參數(shù)奥帘,它不會被運(yùn)行時發(fā)生類型擦除铜邮。注意,帶reified
類型參數(shù)的inline
函數(shù)不能在Java
代碼中調(diào)用寨蹋,普通的內(nèi)聯(lián)函數(shù)可以像常規(guī)函數(shù)那樣在Java
中調(diào)用 - 它們可以被調(diào)用而不能被內(nèi)聯(lián)松蒜。帶實化類型參數(shù)的函數(shù)需要額外的處理,來把類型實參的值替換到字節(jié)碼中已旧,所以它們必須永遠(yuǎn)是內(nèi)聯(lián)的秸苗,這樣它們不可能用Java
那樣普通的方式調(diào)用。
2.3 使用實化類型參數(shù)代替類引用
另一種實化類型參數(shù)的常見使用場景是接收java.lang.Class
類型參數(shù)的API
構(gòu)建適配器运褪。例如JDK
中的ServiceLoader
惊楼,它接收一個代表接口或抽象類的java.lang.Class
玖瘸,并返回實現(xiàn)了該接口的實例。
val serviceImpl = ServiceLoader.load(Service::class.java)
::class.java
的語法展現(xiàn)了如何獲取java.lang.Class
對應(yīng)的Kotlin
類檀咙,這和Java
中的Service.Class
是完全等同的雅倒,現(xiàn)在我們用 帶實化類型參數(shù)的函數(shù) 重寫這個例子:
val serviceImpl = loadService<String>()
loadService
的定義為如下,要加載的服務(wù)類 現(xiàn)在被指定成了loadService
函數(shù)的類型實參:
inline fun <reified T> loadService() {
//把 "T::class" 當(dāng)成類型形參的類訪問弧可。
return ServiceLoader.load(T::class.java)
}
這種用在普通類上的::class.java
語法也可以同樣用在實化類型參數(shù)上蔑匣,使用這種語法會產(chǎn)生對應(yīng)到指定為類型參數(shù)的類的java.lang.Class
,你可以正常地使用它侣诺,最后我們以一個startActivity
的調(diào)用來結(jié)束本節(jié)的討論:
inline fun <reified T : Activity> Context.startActivity {
val intent = new Intent(this, T::class.java)
startActivity(intent)
}
>> startActivity<DetailActivity>()
2.4 實化類型參數(shù)的限制
我們可以按下面的方式來使用實化類型參數(shù)
- 用在類型檢查和類型轉(zhuǎn)換中:
is
殖演、!is
、as
年鸳、as?
- 使用
Kotlin
反射API
,::class
- 獲取對應(yīng)的
java.lang.Class
丸相,::class.java
- 作為調(diào)用其它函數(shù)的類型實參
不能做下面的事情:
- 創(chuàng)建指定為類型參數(shù)的類的實例
- 調(diào)用類型參數(shù)類的伴生對象的方法
- 調(diào)用 帶實化類型參數(shù)函數(shù) 的時候使用 非實化類型形參作為類型實參
- 把類搔确、屬性或者非內(nèi)聯(lián)函數(shù)的類型參數(shù)標(biāo)記為
reified
,因為實化類型參數(shù)只能用在內(nèi)聯(lián)函數(shù)上灭忠,使用實化類型參數(shù)意味著函數(shù)和所有傳給它的lambda
都會被內(nèi)聯(lián)膳算,如果內(nèi)聯(lián)函數(shù)使用lambda
的方法導(dǎo)致lambda
不能被內(nèi)聯(lián),或者你不想lambda
因為性能的關(guān)系被內(nèi)聯(lián)弛作,可以使用noinline
修飾符涕蜂。
三、變型:泛型和子類型化
變型的概念描述了擁有 相同基礎(chǔ)類型 和 不同類型實參 的類型之間是如何關(guān)聯(lián)的映琳,例如List<String>
和List<Any>
之間如何關(guān)聯(lián)机隙。
3.1 為什么存在變型:給函數(shù)傳遞實參
假設(shè)你有一個接受List<Any>
作為實參的函數(shù),那么把List<String>
類型的變量傳遞給這個函數(shù)是否安全呢萨西?我們來看下面兩個例子:
- 第一個例子:
fun printContents(list: List<Any>) {
println(list.joinToString())
}
fun main(args: Array<String>) {
printContents(listOf("abc", "bac"))
}
這上面的函數(shù)可以正常地工作有鹿,函數(shù)把每個元素都當(dāng)作Any
對待,而且因為每個字符都是Any
谎脯,因此這是完全安全的葱跋,運(yùn)行結(jié)果為:
>> abc, bac
- 第二個例子,與之前不同源梭,它會修改列表:
fun addAnswer(list : MutableList<Any>) {
list.add(42)
}
fun main(args: Array<String>) {
val strings = mutableListOf("abc", "bac")
addAnswer(strings)
}
這里聲明了一個類型為MutableList<String>
的變量strings
娱俺,然后嘗試把它傳遞給一個接收MutableList<Any>
的函數(shù),編譯器將不會通過調(diào)用废麻。
因此荠卷,當(dāng)我們將一個字符串列表傳遞給期望Any
對象的列表時,如果 函數(shù)添加或者替換了 列表中的元素(通過MutableList
來推斷)就是不安全的脑溢,因為這樣會產(chǎn)生類型不一致的可能僵朗,否則它就是安全的赖欣。
3.2 類、類型和子類型
變量的類型 規(guī)定了 變量的可能值验庙,有時候我們會把類型和類當(dāng)成同樣的概念使用顶吮,但它們不一樣。
類粪薛、類型
非泛型類
對于非泛型類來說悴了,類的名稱可以直接當(dāng)作類型使用。例如违寿,var x : String
聲明了一個可以保存String
類的實例的變量湃交,而var x : String?
聲明了它的可空類型版本,這意味著 一個Kotlin
類都可以用于構(gòu)造至少兩種類型藤巢。
泛型類
要得到一個合法的類型搞莺,需要首先得到一個泛型類您没,并用一個作為 類型實參的具體類型 替換泛型類的 類型形參泞当。
List
是一個類而不是類型,下面列舉出來的所有替代品都是合法的類型:List<Int>
集歇、List<String?>
和List<List<String>>
绍刮,每一個 泛型類都可能生成潛在的無限數(shù)量的類型温圆。
子類型
子類型的含義為:
任何時候如果需要的是類型
A
的值,能夠使用類型B
的值當(dāng)做A
的值孩革,類型B
就稱為類型A
的子類型岁歉。
例如Int
是Number
的子類型,但Int
不是String
的子類型膝蜈,這個定義還表明了任何類型都可以被認(rèn)為是它自己的子類型锅移。
超類型
超類型 是 子類型 的反義詞
如果
A
是B
的子類型,那么B
就是A
的超類型彬檀。
編譯器在每一次給變量賦值或者給函數(shù)傳遞實參的時候都要做這項檢查:
- 只有 值的類型 是 變量類型的子類型 時帆啃,才允許存儲變量的值
- 只有當(dāng) 表達(dá)式的類型 是 函數(shù)參數(shù)的類型的子類型 時,才允許把該表達(dá)式傳給函數(shù)
子類窍帝、子類型
在簡單情況下努潘,子類和子類型本質(zhì)上是一樣的,例如Int
類是Number
的子類坤学,因此Int
類型是Number
類型的子類型疯坤。
一個非空類型是它的可空版本的子類型,但它們都對應(yīng)著同一個類深浮,你始終能夠在可空類型的變量中存儲非空類型值压怠。
當(dāng)開始涉及泛型類時,子類型和子類之間的差異就顯得格外重要飞苇。正如我們上面見到的菌瘫,MutableList<String>
不是MutableList<Any>
的子類型蜗顽。
對于泛型類
MutableList
而言,無論A
和B
是什么關(guān)系雨让,MutableList<A>
既不是MutableList<B>
的子類型也不是它的超類型雇盖,它就被稱為 在該類型參數(shù)上是不變型的。
Java
中的所有類都是不變型的栖忠。在前一節(jié)中崔挖,我們見到了List
類,對它來說庵寞,子類型化規(guī)則不一樣狸相,Kotlin
中的List
接口表示的是只讀集合。如果A
是B
的子類型捐川,那么List<A>
就是List<B>
的子類型脓鹃,這樣的類或者接口被稱為 協(xié)變的。
3.3 協(xié)變:保留子類型化關(guān)系
一個協(xié)變類是一個泛型類属拾,如果
A
是B
的子類型将谊,那么Producer<A>
就是Producer<B>
的子類型,我們說 子類型化被保留了渐白。
在Kotlin
中,要聲明類在某個類型參數(shù)上是可以協(xié)變的逞频,在該類型參數(shù)的名稱前加上out
關(guān)鍵字即可纯衍,下面例子就可以表達(dá)為:Producer
類在類型參數(shù)T
上是可以協(xié)變的。
interface Producer<out T> {
fun produce() : T
}
將一個類的類型參數(shù)標(biāo)記為協(xié)變的苗胀,在 該類型實參沒有精確匹配到函數(shù)中定義的類型形參時襟诸,可以讓該類的值作為這些函數(shù)的實參傳遞,也可以作為這些函數(shù)的返回值基协。
你不能把任何類都變成協(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
的值。
因此類型參數(shù)T
上的關(guān)鍵字有兩層含義:
- 子類型化會被保留著榴,即前面談到的
Producer<Cat>
是Producer<Animal>
的子類型 -
T
只能用在out
位置
在構(gòu)造方法的參數(shù)上使用 out
構(gòu)造方法的參數(shù)既不在in
位置添履,也不再out
位置,即使類型參數(shù)聲明成了out
兄渺,仍然可以在構(gòu)造方法參數(shù)的聲明中使用它缝龄。
class Herd<out T : Animal> (vararg animals : T) { ... }
如果把類的實例當(dāng)成一個更泛化的類型的實例使用,變型會防止該實例被誤用挂谍,不能調(diào)用存在潛在危險的方法叔壤。構(gòu)造方法不是那種在實例創(chuàng)建之后還能調(diào)用的方法,因此它不會有潛在的危險口叙。
然而炼绘,如果你在構(gòu)造方法的參數(shù)上使用了關(guān)鍵字var
和val
,同時就會聲明一個getter
和setter
妄田,因此俺亮,對只讀屬性來說,類型參數(shù)用在了out
位置疟呐,而可變屬性在out
和in
位置都使用了它脚曾。
class Herd<T : Animal> (var leadAnimal : T, vararg animals : T) { ... }
上面這個例子中,T
不能用out
標(biāo)記启具,因為類包含屬性leadAnimal
的setter
本讥,它在in
位置用到了T
。
位置規(guī)則只覆蓋了類外部可見的 API
位置規(guī)則只覆蓋了類外部可見的api
鲁冯,私有方法的參數(shù)既不在in
位置拷沸,也不在out
位置,變型規(guī)則只會防止外部使用者對類的誤用薯演,但不會對類自己的實現(xiàn)起作用撞芍。
class Herd<out T : Animal> (private var leadAnimal : T, vararg animals : T) { ... }
現(xiàn)在可以安全地讓Herd
在T
上協(xié)變,因為屬性leadAnimal
被聲明成了私有跨扮。
3.4 逆變:反轉(zhuǎn)子類型化關(guān)系
逆變的概念可以看成是協(xié)變的鏡像序无,對一個逆變類來說,它的子類型化關(guān)系與用作類型實參的類的子類型化關(guān)系是相反的:如果
B
是A
的子類型好港,那么Consumer<A>
就是Consumer<B>
的子類型愉镰。
以Comparator
接口為例,這個接口定義了一個compare
方法钧汹,用于比較兩個指定的對象:
interface Comparator<in T> {
fun compare(e1 : T, e2 : T) : Int { ... }
}
這個接口方法只是消費(fèi)類型為T
的值丈探,這說明T
只在in
位置使用,因此它的聲明之前用了in
關(guān)鍵字拔莱。
一個為特定類型的值定義的比較器顯然可以比較該類型任意子類型的值碗降,例如隘竭,如果有一個Comparator<Any>
,可以用它比較任意具體類型的值讼渊。
val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
val strings : List<String> = ...
strings.sortedWith(anyComparator)
sortedWith
期望一個Comparator<String>
动看,傳給它一個能比較更一般的類型的比較器是安全的。如果你要在特定類型的對象上執(zhí)行比較爪幻,可以使用能處理該類型或者它的超類型的比較器菱皆。
這說明Comparator<Any>
是Comparator<String>
的子類型,其中Any
是String
的超類型挨稿。不同類型之間的子類型關(guān)系 和 這些類型的比較器之間的子類型關(guān)系 截然相反仇轻。
in
關(guān)鍵字的意思是,對應(yīng)類型的值是傳遞進(jìn)來給這個類的方法的奶甘,并且被這些方法消費(fèi)篷店。和協(xié)變的情況類似,約束類型參數(shù)的使用將導(dǎo)致特定的子類型化關(guān)系臭家。
一個類可以在一個類型參數(shù)上協(xié)變疲陕,同時在另外一個類型參數(shù)上逆變。Function
接口就是一個經(jīng)典的例子:
interface Function1<in P, out R> {
operator fun invoke(p : P) : R
}
這意味著對這個函數(shù)類型的第一類型參數(shù)來說钉赁,子類型化反轉(zhuǎn)了蹄殃,而對于第二個類型參數(shù)來說,子類型化保留了你踩。例如窃爷,你有一個高階函數(shù),該函數(shù)嘗試對你所有的貓進(jìn)行迭代姓蜂,你可以把一個接收動物的lambda
傳遞給它。
fun enumerate(f : (Cat) -> Number) { ... }
fun Animal.getIndex() : Int = ...
>> enumerate(Animal :: getIndex)
更多文章医吊,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:http://www.reibang.com/p/fd82d18994ce
- 個人主頁:http://lizejun.cn
- 個人知識總結(jié)目錄:http://lizejun.cn/categories/