參考:
- Kotlin 實戰(zhàn)
- Java 泛型推薦閱讀:https://www.zhihu.com/question/20400700
代碼與說明
Kotlin 分享系列,來自部門團(tuán)隊幾個伙伴一起整理,全部MD與代碼都在:
https://github.com/zhaoyubetter/KotlinShare
Kotlin 泛型基本上與Java泛型類型,了解了Java泛型,基本就了解了Kotlin的泛型
Kotlin 泛型內(nèi)容包括:
- 泛型函數(shù)與類
- 類型擦除與實化類型參數(shù)
- 聲明點變形與使用點型變
實化類型參數(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()
}