Kotlin教程(九)泛型

寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使本身記憶和理解的更加深入,二是能夠分享給一樣想學習Kotlin的同窗。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展現出來同時,我也會添加對應的Java代碼用於對比學習和更好的理解。java

Kotlin教程(一)基礎
Kotlin教程(二)函數
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運算符重載及其餘約定
Kotlin教程(八)高階函數
Kotlin教程(九)泛型編程


泛型類型參數

泛型容許你定義帶類型形參的類型,當這種類型的實例被建立出來的時候,類型形參被替換成稱爲類型實參的具體類型。例如:安全

List<String>
Map<String, Person>
複製代碼

和通常類型同樣,Kotlin編譯器也經常能推導出類型實參:bash

val authors = listOf("Dimtry", "Sevelana")
複製代碼

若是你想建立一個空的列表,這樣就沒有任何能夠推導出類型實參的線索,你就得顯式地指定它(類型形參)。app

val readers: MutableList<String> = mutableListOf()

val readers = mutableListOf<String>()
複製代碼

和Java不一樣,Kotlin始終要求類型實參要麼被顯式地說明,要麼能被編譯器推導出來。由於泛型是1.5版本才引入到Java的,它必須保證和基於老版本的兼容,因此它容許使用沒有類型參數的泛型類型——所謂的原生態類型。而Kotlin從一開始就有泛型,因此它不支持原生態類型,類型實參必須定義。dom

泛型函數和屬性

若是要編寫一個使用列表的函數,但願它能夠在任何列表上使用,而不是某個具體類型的元素的列表,須要編寫一個泛型函數。ide

fun <T> List<T>.slice(indices: IntReange): List<T>
複製代碼

基本上是和Java的聲明相似的,在方法名前聲明,便可在函數中使用。函數

還能夠給類或接口的方法,頂層函數,擴展屬性以及擴展函數聲明類型參數。例以下面你這個返回列表倒數第二個元素的擴展屬性:工具

val <T> List<T>.penultimate: T
    get() = this[size -2]
複製代碼

不能聲明泛型非擴展屬性post

普通(非擴展)屬性不能擁有類型參數,不能再一個類的屬性中存儲多個不一樣類型的值,所以聲明泛型非擴展函數函數沒有任何意義。

聲明泛型類

和Java同樣,Kotlin經過在類名稱後面加上一對尖括號,並把類型參數放在尖括號內來聲明泛型類及泛型接口。一旦聲明以後,就能夠在類的主體內像其餘類型同樣使用類型參數。

interface List<T> {
    operator fun get(index: Int): T
}
複製代碼

若是你的類繼承了泛型(或者實現了泛型接口),你就得爲基礎類型的泛型形參提供一個類型實參。

class StringList: List<String> {
    override fun get(index: Int): String = ...
}
複製代碼

類型參數約束

類型參數約束能夠限制做爲(泛型)類和(泛型)函數的類型實參的類型。 若是你把一個類型指定爲泛型類型形參的上界約束,在泛型類型具體的初始化中,其對應的類型實參就必須是這個具體類型或其子類型。你是這樣定義約束:把冒號放在類型參數名稱以後,做爲類型形參上界的類型緊隨其後:

fun <T : Number> List<T>.sum(): T
複製代碼

至關於Java中的:

<T extends Number> T sum(List<T> list)
複製代碼

一旦指定了類型形參T的上界,你就能夠把類型T的值當作它的上界的值使用:

fun <T : Number> oneHalf(value: T): Double {
    return value.toDouble() //調用Number的方法
}
複製代碼

極少數狀況下,須要在一個類型參數上指定多個約束,這時你須要使用不一樣的語法:

fun <T> ensureTrailingPeriod(seq: T) 
    where T : CharSequence, T : Appendable {
    if(!seq.endWith('.') { //調用CharSequence的方法
        seq.append('.')//調用Appendable的方法
    }
}
複製代碼

這種狀況下,能夠說明做爲類型實參的類型必須同時實現CharSequence和Appendable兩個接口。

讓類型形參非空

若是你聲明的時泛型類或者泛型函數,任何類型實參,包括哪些可空的類型實參,均可以替換她的類型形參。事實上沒有指定上界的類型形參將會使用Any? 這個默認上界:

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

process函數中,參數value是可空的,儘管T並無使用問號標記。

若是你想保證替換類型形參的始終是非空類型,能夠經過制定一個約束來實現。若是你除了可空性以外沒有任何限制,可使用Any代替默認的Any?做爲上界。

class Processor<T : Any> {
    fun process(value: T) {
        value.hashCode()
    }
}
複製代碼

運行時的泛型:擦除和實化類型參數

你可能知道,JVM上的泛型通常是經過類型擦除實現的,就是說泛型類實例的類型實參在運行時是不保留的。

運行時的泛型:類型檢查和轉換

和Java同樣,Kotlin的泛型在運行時也被擦除了。這意味着泛型類實例不會攜帶用於建立它的類型實參的信息。例如,若是你建立一個List<String>並將一堆字符串放到其中,在運行時你只能看到它是一個List,不能識別出列表本打算包含的時哪一種類型的元素。
隨着擦除類型信息也帶來了約束。由於類型實參沒有被存儲下來,你不能檢查他們。例如,你不能判斷一個列表是一個包含字符串的列表仍是包含其餘對象的列表:

>>> if (value is List<String>)
ERROR: Canot check for instance of erased type
複製代碼

那麼如何檢查一個值是不是列表,而不是set或者其餘對象。可使用特殊的*投影語法來作這樣的檢查:

if (value is List<*>)
複製代碼

這種表示擁有未知類型實參的泛型類型,相似於Java中的List<?>

注意,在asas?轉換中仍然可使用通常的泛型類型。可是若是該類有正確的基礎類型但類型實參是錯誤的,轉換也不會失敗,由於在運行時轉換髮生的時候類型實參是未知的。所以,這樣的轉換會致使編譯器發出「unchecked cast」的警告。這僅僅是一個警告,你仍然能夠繼續使用這個值。

fun printSum(c: Collection<*>) {
    //這裏會有警告:Unchecked cast:List<*> to List<Int>
    val intList = c as? List<Int> 
            ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

>>> printSum(listOf(1, 2, 3))
6
複製代碼

編譯一切正常:編譯器只是發出了一個警告,這意味着代碼是合法的。若是在一個整型的列表或者set上調用該函數,一切都會如預期發生:第一種狀況會打印元素之和,第二種狀況會拋出IllegalArgumentException異常。但若是你傳遞了一個錯誤類型的值,如List<String>,運行時會獲得一個ClassCastException。

聲明帶實化類型參數的函數

前面說過,Kotlin泛型在運行時會被擦除,泛型函數的類型實參也是這樣。在調用泛型函數的時候,在函數體中你不能決定調用它用的類型實參:

>>> fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T
複製代碼

一般狀況下都是這樣的,只有一種例外能夠避免這種限制:內聯函數。內聯函數的類型形參可以被實化,意味着你能夠在運行時引用實際的類型實參。
在以前章節中,咱們知道若是用inline關鍵字標記一個函數,編譯器會把每一次函數調用都換成函數實際的代碼實現。使用內聯函數還能夠提高性能,若是該函數使用了lambda實參:lambda的代碼也會內聯,因此不會建立任何匿名類。基於這種實現原理,應該也能夠想象到,根據嵌入的上下文,泛型在class文件中已經被肯定了。
若是把前面例子中的isA函數聲明成inline而且用reified標記類型參數,你就可以用該函數檢查value是否是T的實例了。

inline fun <reified T> isA(value: Any) = value is T

>>> println(isA<String>("abc"))
true
>>> println(isA<String>(123))
false
複製代碼

一個實化類型參數能發揮做用的最簡單的例子就是標準庫函數filterIsInstance 。這個函數接收一個集合,選擇其中哪些指定類的實例,而後返回這些被選中的實例。

>>> val items = listOf("one", 2, "three")
>>> println(items.filterIsInstance<String>())
[one, three]
複製代碼

經過指定<String>做爲函數的類型實參,你代表感興趣的只是字符串。所以函數的返回類型是List<String>。這種狀況下,類型實參在運行時是已知的,函數filterIsInstance使用它來檢查列表中的值是否是指定爲該類型實參的類的實例。
下面是Kotlin標準庫函數filterIsInstance聲明的簡化版本:

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
    return destination
}
複製代碼

在以前章節,咱們提到把函數標記成內聯只有在一種狀況下有性能優點,即函數擁有函數類型的形參而且其對應的實參lambda和函數一塊兒被內聯的時候。而如今咱們是爲了可以使用實化參數而把函數標記成內聯。

爲何實化只對內聯函數有效

編譯器把實現內聯函數的字節碼插入每一次調用發生的地方。每次你調用帶實化類型參數的函數時,編譯器都知道此次特定調用中用做類型實參的切確類型。所以,編譯器能夠生成引用做爲類型實參的具體類的字節碼。實際對filterIsInstance<String>掉用來講,生成的代碼和下面這段代碼是等價的:

for (element in this) {
    if (element is String) {
        destination.add(element)
    }
}
複製代碼

由於生成的字節碼引用了具體類,而不是類型參數,它不會被運行時發生的類型參數擦除影響。
注意,帶reified類型參數的inline函數不能再Java代碼中調用。 普通內聯函數能夠像常規函數那樣在Java中調用——他們能夠被調用而不能被內聯。帶實化參數類型的函數須要額外的處理,來把類型參數的值替換到字節碼中,因此他們必須永遠是內聯的。這樣他們不可能用Java那樣的普通方式調用。

使用實化類型參數代替類引用

若是你是Android開發者,顯示Activity是一個最經常使用的方法。也可使用實化類型參數來代替傳遞做爲java.lang.Class的Activity類:

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

>>> startActivity
複製代碼

::class.java的語法展示瞭如何獲取java.lang.Class對應的Kotlin類。這和Java中的Service.class是徹底等同的。

實化類型參數的限制

儘管實化類型參數是方便的工具,但它們也有一些限制。有一些事實化與生俱來的,而另一些則是現有的實現決定的,並且可能在將來的Kotlin版本中放鬆這些限制。
具體來講,能夠按下面的方式使用實化類型參數:

  • 用在類型檢查和類型轉換中(is!isasas?
  • 使用Kotlin反射API(::class
  • 獲取相應的java.lang.Class::class.java
  • 做爲調用其餘函數的類型實參

不能作下面的這些事情:

  • 建立指定爲類型參數的類的實例
  • 調用類型參數類的伴生對象的方法
  • 調用帶實化類型參數函數的時候使用非實化類型形參做爲類型實參
  • 把類、屬性或者非內聯函數的類型參數標記成reified

變型:泛型和子類型化

變型的概念描述了擁有相同基礎類型和不一樣類型實參的(泛型)類型之間是如何關聯的:例如,List<String>List<Any>之間如何關聯。

爲何存在變型:給函數傳遞實參

假如你有一個接收List<Any>做爲實參的函數。把List<String>類型的變量傳給這個函數時候安全?毫無疑問,把一個字符串傳給一個指望Any的函數是安全的,由於String繼承了Any。但當String和Any變成List接口的類型實參以後,狀況就沒有這麼簡單了。

fun printContents(list: List<Any>) {
    println(list.joinToString())
}

>>> printContents(listOf("abc", "bac"))
abc, bac
複製代碼

這看上去沒什麼問題,咱們來看另外一個例子:

fun addAnswer(list: MutableList<Any>) {
    list.add(42)
}

>>> val strings = mutableListOf("abc", "bac")
>>> addAnswer(strings)
Type mismatch. Required: MutableList<Any> Found: MutableList<String>
複製代碼

這個例子和上面的例子中,區別僅僅是將List<Any>變成了MutableList<Any>,就沒法將泛型爲String的list傳遞給函數。
如今能夠回答剛纔那個問題了,把一個字符串列表傳給指望Any對象列表的函數是否安全。若是函數添加或者替換了列表中的元素就是不安全的,由於這樣會產生類型不一致的可能性。在Kotlin中,能夠經過根據列表是否可變選擇合適的接口來輕鬆的控制。若是函數接收的是隻讀列表,能夠傳遞具備更具體的元素類型的列表。若是列表是可變的,就不能這麼作。

類、類型和子類型

爲了討論類型之間的關係,須要熟悉子類型這個術語。任什麼時候候若是須要的時類型A的值,你都可以使用類型B的值(當作A的值),類型B就稱爲類型A的子類型。舉例來講,Int是Number的子類型,但Int不是String的子類型。這個定義還標明瞭任何類型均可以被認爲是它本身的子類型。
術語超類型是子類型的反義詞。若是A是B的子類型,那麼B就是A的超類型。
爲何一個類型是不是另外一個的子類型這麼重要?編譯器在每一次給變量賦值或者給函數傳遞實參的時候都要作這項檢查。

fun test(i: Int) {
    val n: Number = i  //能夠編譯
    fun f(s: String) {/*...*/}
    f(i)  //不能編譯
}
複製代碼

只有值得類型是變量類型的子類型時,才容許變量存儲該值。例如,變量n的初始化器i的類型Int是變量的類型Number的子類型,因此n的聲明是合法的。只有當表達式的類型是函數參數的類型的子類型時,才容許把該表達式傳給函數。這個例子中i的類型Int不是函數參數的類型String的子類型,因此函數f的調用會編譯失敗。
你可能認爲子類型就是子類的概念,可是爲何在Kotlin中稱之爲子類型呢?由於,Kotlin存在可空類型。一個非空類型是它的可空版本的子類型,但它們都對應着同一個類。你始終能在可空類型的變量中存儲非空類型的值,但反過來卻不行。

var s: String = "abc"
val t: String? = s //編譯經過
s = t  //編譯不經過
複製代碼

前面,咱們把List<String>類型的變量傳給指望List<Any>的函數是否安全,如今可使用子類型化術語來從新組織:List<String>List<Any>的子類型嗎?你已經瞭解了爲何把MutableList<String>當成MutableList<Any>的子類型對待是不安全的。顯然,返回來也是不成立的:MutableList<Any>確定不是MutableList<String>的子類型。
一個泛型類(例如MutableList)若是對於任意兩種類型A和B,MutableList<A>既不是MutableList<B>的子類型也不是他的超類型,他就是被稱爲在該類型參數上是不變型的。Java中全部的類都是不變型的(儘管哪些類具體的使用能夠標記成可變型的,稍後你就會看到)。

List類的類型化規則不同,Kotlin中的List接口表示的是隻讀集合,若是A是B的子類型,那麼List<A>就是List<B>的子類型。這樣的類或者接口被稱爲協變的。

協變:保留子類型化關係

一個協變類是一個泛型類(咱們以Producer<T>爲例),對這種類來講,下面的描述是成立的:若是A是B的子類型,那麼Producer<A>就是Producer<B>的子類型。咱們說子類型化被保留了。
在Kotlin中,要聲明類在某個類型參數上是能夠協變的,在該類型參數的名稱前面加上out關鍵字便可:

interface Producer<out T> {
    fun produce(): T
}
複製代碼

將一個類的類型參數標記爲協變得,在該類型實參沒有精確匹配到函數中定義的類型形參時,可讓該類的值做爲這些函數的實參傳遞,也能夠做爲這些函數的返回值。例如,想象一下有這樣一個函數,它負責餵養用類Herd表明的一羣動物,Herd類的類型參數肯定了畜羣中動物的類型。

open class Animal {
    fun feed() {...}
}

class Herd<T : Animal> {
    val size: Int
        get() = ...

    operator fun get(i: Int): T {...}
}

fun feeAll(animals: Herd<Animal>) {
    for (i in 0 until animals.size) {
        animals[i].feed()
    }
}
複製代碼

假設這段代碼的用戶有一羣貓須要照顧:

class Cat : Animal() {
    fun cleanLitter() {...}
}

fun takeCareOfCats(cats: Herd<Cat>) {
    for(i in 0 until cats.size) {
        cats[i].cleanLitter()
        // feedAll(cats)  //錯誤:類型不匹配
    }
}
複製代碼

若是嘗試把貓羣傳給feedAll函數,在編譯期你就會獲得類型不匹配的錯誤。由於Herd類中的類型參數T沒有用任何變型修飾符,貓羣不是畜羣的子類。可使用顯示得類型轉換來繞過這個問題,可是這種方法囉嗦、易出錯,並且幾乎歷來不是解決類型不匹配問題的正確方式。
由於Herd類有一個相似List的API,而且不容許它的調用者添加和改變畜羣中的動物,能夠把它變成協變並相應地修改調用代碼。

class Herd<out T: Animal> {
    ...
}
複製代碼

你不能把任何類都變成協變得:這樣不安全。讓類在某個類型參數變爲協變,限制了該類中對該類型參數使用的可能性。要保證類型安全,它只能用在所謂的out位置,意味着這個類只能生產類型T的值而不能消費它們。
在類成員的聲明中類型參數的使用能夠分爲in位置和out位置。考慮這樣一個類,它聲明瞭一個類型參數T幷包含了一個使用T的函數。若是函數是把T當成返回類型,咱們說它在out位置。這種狀況下,該函數生產類型爲T的值。若是T用做函數參數的類型,它就在in位置,這樣的函數消費類型爲T的值。

interface Transformer<T> {
                //in位置 //out位置
    fun transform(t: T): T
}
複製代碼

類的類型參數前的out關鍵字要求全部使用T的方法只能把T放在out位置而不能放在in位置。這個關鍵字約束了使用T的可能性,這保證了對應子類型關係的安全性。

重申一下,類型參數T上的關鍵字out有兩層含義:

  • 子類型化被保留
  • T只能用在out位置

如今咱們看看List<Interface>接口。Kotlin的List是隻讀的,因此它只有一個返回類型爲T的元素的方法get,而沒有定義任何把類型爲T的元素存儲到列表中的方法。所以,它也是協變的。

interface List<out T> : Collection<T> {
    operator fun get(index: Int): T
}
複製代碼

注意,類型形參不光能夠直接看成參數類型或者返回類型使用,還能夠看成另外一個類型的類型實參。例如,List接口就包含了一個返回List<T>的subList方法:

interface List<out T> : Collection<T> {
    fun subList(fromIndex: Int, toIndex: Int): List<T>
}
複製代碼

在這個例子中,函數subList中的T也用在out位置。
注意,不能把MutableList<T>在它的類型參數上聲明成協變的,由於它既含有接收類型爲T的值做爲參數的方法,也含有返回這種值得方法(所以,T出如今in和out兩種位置上)。

interface MutableList<T>
        : List<T>, MultableCollection<T> {
    override fun add(element: T): Boolean   
}
複製代碼

編譯器強制實施了這種限制。若是這個類被聲明成協變得,編譯器會報錯:Type parameter T is declared as 'out' but occurs in 'in' position(類型參數T聲明爲out但出如今in位置)。
注意,構造方法的參數即不在in位置,也不在out位置。即便類型參數聲明成了out,仍然能夠在構造方法參數的聲明中使用它:

class Herd<out T: Animal>(vararg animals: T) {...}
複製代碼

若是把類的實例當成一個更泛化的類型的實例使用,變型會防止該實例被誤用:不能調用存在潛在危險的方法。構造方法不是那種在實例建立以後還能調用的方法,所以它不會有潛在危險。
而後,若是你在構造方法的參數上使用了關鍵字val和var,同時就會聲明一個getter和一個setter(若是屬性是可變的)。所以,對只讀屬性來講,類型參數用在了out位置,而可變屬性在out位置和in位置都使用了它:

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) {...}
複製代碼

上面這個例子中,T不能用out標記,由於類包含屬性leadAnimal的setter,它在in位置用到了T。
還須要注意的是,位置規則只覆蓋了類外部可見的(public、protected和internal)API。私有方法的參數即不在in位置也不在out位置。變型規則只會防止外部使用者對類的誤用但不會對類本身的實現起做用:

class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) {...}
複製代碼

如今能夠安全地讓Herd在T上協變,由於屬性leadAnimal變成了私有的。

逆變:反轉子類型化關係

逆變的概念能夠被當作是協變的鏡像:對一個逆變來講,它的子類型化關係與用做類型實參的類的子類型化關係是相反的。咱們從Comparator接口的例子開始,這個接口定義了一個方法compare類,用於比較兩個給定的對象:

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int {...}
}
複製代碼

一個爲特定類型的值定義的比較器顯然能夠比較該類型任意子類型的值。例如,若是有一個Comparator<Any>,能夠用它比較任意具體類型的值。

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int
}

fun main(args: Array<String>) {
    val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
    val strings = listOf("a", "b", "c")
    strings.sortedWith(anyComparator)
}
複製代碼

sortedWith函數指望一個Comparator<String>(一個能夠比較字符串的比較器),傳給它一個能比較更通常的類型的比較器是安全的。若是你要在特定類型的對象上執行比較,可使用能處理該類型或者它的超類型的比較器。這說明Comparator<Any>Comparator<String>的子類型,其中Any是String的超類型。不一樣類型之間的子類型關係和這些類型的比較器之間的子類型化關係截然相反。
如今你已經爲完整的逆變定義作好了準備。一個在類型參數上逆變的類是這樣的一個泛型類(咱們以Consumer<T>爲例),對這種類來講,下面的描述是成立的:若是B是A的子類,那麼Consumer<A>就是Consumer<B>的子類型,類型參數A和B交換了位置,因此咱們說子類型化被反轉了。

in關鍵字的意思是,對應類型的值是傳遞進來給這個類的方法的,而且被這些方法消費。和協變得狀況相似,約束類型參數的使用將致使特定的子類型化關係。在類型參數T上的in關鍵字意味着子類型化被反轉了,並且T只能用在in位置。

協變得,逆變的和不變型的類

協變 逆變 不變型
Producer<out T> Consumer<in T> MutableList<T>
類的子類型化保留了:Producer<Cat>Producer<Animal>的子類型 子類型化反轉了:Consumer<Animal>Consumer<Cat>的子類型 沒有子類型化
T 只能在out位置 T只能在in位置 T能夠在任何位置

一個類能夠在一個類型參數上協變,同時在另一個類型參數上逆變。Function接口就是一個經典的例子。下面是一個單個參數的Function的聲明:

interface Function1<in P, out R> {
    operator fun invoke(p: P): R
}
複製代碼

Kotlin的表達發(P) -> R是表達Function<P, R>的另外一種更具可讀性的形式。能夠發現用in關鍵字標記的P(參數類型)只用在in位置,而用out關鍵字標記的R(返回類型)只用在out位置。這意味着對這個函數類型的第一個類型參數來講,子類型化反轉了,而對於第二個類型參數來講,子類型化保留了。

fun enumerateCats(f: (Cat) -> Number) {...}
fun Animal.getIndex(): Int = ...
>>> enumerateCats(Animal::getIndex)
複製代碼

在Kotlin中這點代碼是合法的。Animal是Cat的超類型,而Int是Number的子類型。

使用點變型:在類型出現的地方指定變型

在類聲明的時候就可以指定變型修飾符是很方便的,由於這些修飾符會應用到全部類被使用的地方。這被稱做聲明點變型。若是你熟悉Java的通配符類型(? extends 和 ? super),你會意識到Java用徹底不一樣的方式處理變型。在Java中,每一次使用帶類型參數的類型的時候,還能夠指定這個類型參數是否能夠用他的子類型或者超類型替換。這叫作使用點變型。

Kotlin的聲明點變型 vs. Java通配符

聲明點變形帶來了更簡潔的代碼,由於只用指定一次變型修飾符,全部這個類的使用者都不用再考慮這些了,在Java中,庫做者不得不一直使用通配符:Function<? super T, ? extends R>,來建立按照用戶指望的運行的API。若是你查看Java 8標準庫的源碼,你會在每次用到Function接口的地方發現通配符。例如,下面是Stream.map方法的聲明:

/* Java */
public interface Stream<T> {
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
}
複製代碼

Kotlin也支持使用點變型,容許在類型參數出現的具體位置指定變型,即便在類型聲明時它不能被聲明成協變或逆變的。
你已經見過許多像MutableList這樣的接口,一般狀況下即不是協變也不是逆變的,由於它同時生產和消費指定爲它們類型參數的類型的值。但對於這個類型的變量來講,在某個特定函數中只被當成其中一種角色使用的狀況挺常見的:要麼是生產者要麼是消費者。例以下面這個簡單的函數:

fun <T> copyData(source: MutableList<T>, 
        destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}
複製代碼

這個函數從一個集合把元素拷貝到另外一個集合中。儘管兩個集合都擁有不變型的類型,來源集合只是用於讀取,而目標集合只是用於寫入。這種狀況下,集合元素的類型不須要精確匹配。例如,把一個字符串的集合拷貝到能夠包含任意對象的集合中一點兒問題也沒有。
要讓這個函數支持不一樣類型的列表,能夠引入第二個泛型參數。

fun <T : R, R> copyData(source: MutableList<T>,
                destination: MutableList<R>) {
    for (item in source) {
        destination.add(item)
    }
}

>>> val ints = mutableListOf(1, 2, 3)
>>> val anyItems = mutableListOf<Any>()
>>> copyData(ints, anyItems)
>>> println(anyItems)
[1, 2, 3]
複製代碼

你聲明瞭兩個泛型參數表明來源列表和目標列表中的元素類型。爲了可以把一個列表中的元素拷貝到另外一個列表中,來源元素類型應該是目標列表中的元素的子類型(Int是Any的子類型)。
可是Kotlin提供了一種更優雅的表達方式。當函數的實現調用了那些類型參數只出如今out位置(或只出如今in位置)的方法時,能夠充分利用這一點,在函數定義中給特定用途的類型參數加上變型修飾符。

fun <T> copyData(source: MutableList<out T>,
                 destination: MutableList<T>) {
    for (item in source) {
        destination.add(item)
    }
}
複製代碼

能夠爲類型聲明中類型參數任意的用法指定變型修飾符,這些用法包括:形參類型、局部變量類型、函數返回類型,等等。這裏發生的一切被稱做類型投影:咱們說source不是一個常規的MutableList,而是一個投影(受限)的MutableList。只能調用返回類型是泛型類型參數的那些方法,或者嚴格的講,只在out位置使用它的方法。編譯器禁止調用使用類型參數作實參的那些方法(在in位置使用類型參數):

>>> val list: MutableList<out Number> = ...
>>> list.add(42)
Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add (element: E): Boolean'
複製代碼

不要爲使用投影類型後不能調用某些方法而吃驚,若是須要調用那些方法,你要用的時常規類型而不是投影。這可能要求你聲明第二個類型參數,它依賴的本來要進行投影的類型。

固然,實現copyData函數的正確方式應該是使用List<T>做爲source實參的類型,由於咱們只用了聲明在List中的方法,並無用到MutableList中的方法,並且List類型參數的變型在聲明時就指定了。但這個例子對展現這個概念依然十分重要,尤爲是要記住大多數的類並無像List和MutableList這樣分開的兩個接口,一個是協變的讀取接口,另外一個是不變型的讀取/寫入接口。
若是類型參數已經有out變型,獲取它的out投影沒有任何意義。就像List<out T>這樣。它和List<T>是一個意思,由於List已經聲明成了class List<out T>。編譯器會發出警告,標明這樣的投影是多餘的。

同理,能夠對類型參數的用法使用in修飾符,來代表在這個特定的地方,相應的值擔當的時消費者,並且類型參數可使用它的任意子類型替換。

fun <T> copyData(source: MutableList<T>,
                 destination: MutableList<in T>) {
    for (item in source) {
        destination.add(item)
    }
}
複製代碼

Kotlin的使用點變型直接對應Java的限界通配符。Kotlin中的MutableList<out T>和Java中的MutableList<? extends T>是一個意思。in投影的MutableList<in T>對應到Java的MutableList<? super T>

星號投影:使用*代替類型參數

本章前面提到類型檢查和轉換的時候,咱們提到了一種特殊的星號投影語法,能夠用它來標明你不知道關於泛型實參的任何信息。例如,一個包含未知類型的元素的列表用這種語法表示爲List<*>。如今咱們深刻探討星號投影的語義。

首先須要注意的是MutableList<*>MutableList<Any?>不同。你確信MutableList<Any?>這種列表包含的時任意類型的元素。而另外一方面,MutableList<*>是包含某種特定類型元素的列表,可是你不知道是哪一個類型。這種列表被建立成一個包含某種特定類型元素的列表,好比String,並且建立它的代碼指望只包含那種類型的元素。由於不知道是哪一個類型,你不能像列表中寫入任何東西,由於你寫入的任何值均可能會違反調用代碼的指望。可是從列表中讀取元素是可行的,由於你內心有數,全部的存儲在列表中的值都能匹配全部Kotlin類型的超類型Any?:

fun main(args: Array<String>) {
    val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
    val chars = mutableListOf('a', 'b', 'c')
    val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
//    unknownElements.add(42) //編譯器禁止調用這個方法
    println(unknownElements.first()) //讀取元素是安全的
}

//輸出
a
複製代碼

爲何編譯器會把MutableList<*>當成out投影的類型?在這個例子的上下文中,MutableList<*>投影成了MutableList<out Any?>,當你沒有任何元素類型信息的時候,讀取Any?類型的元素任然是安全的,可是向列表中寫入元素是不安全的。
Kotlin的MyType<*>至關於Java中的MyType<?>

對像Consumer<in T>這樣的逆變類型的參數來講,星號投影等價於<in Nothing>。實際上,在這種星號投影中沒法調用任何簽名中有T的方法。若是類型參數是逆變的,它就只能表現爲一個消費者,並且,咱們以前討論過,你不知道它能夠消費的究竟是什麼。所以,不能讓它消費任何東西。

當類型實參的信息並不重要的時候,可使用星號投影的語法,不須要使用任何在簽名中引用類型參數的方法,或者只是讀取數據額不關心它的具體類型。例如,能夠實現一個接收List<*>作參數的printFirst函數:

fun printFirst(list: List<*>) {
    if (list.isNotEmpty()) {
        println(list.first())
    }
}
複製代碼
相關文章
相關標籤/搜索