Kotlin 知識梳理(13) 運行時的泛型

1、本文概要

本文是對<<Kotlin in Action>>的學習筆記,若是須要運行相應的代碼能夠訪問在線環境 try.kotlinlang.org,這部分的思惟導圖爲: java

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

2.1 運行時的泛型

Java同樣,Kotlin的泛型在運行時也被擦除了,這意味着 泛型類實例不會攜帶用於建立它的類型實參的信息api

例如,若是你建立了一個List<String>,在運行時你只能看到它是一個List,不能識別出列表本打算包含的是String類型的元素。安全

接下來咱們談談伴隨着擦除類型信息的約束,由於類型實參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<*>)
複製代碼

實際上,泛型類型擁有的每一個類型形參都須要一個*,如今你能夠認爲它就是 擁有未知類型實參的泛型類型this

asas?轉換中仍然可使用通常的泛型類型,可是若是該類 有正確的基礎類型但類型實參是錯誤的,轉換也不會失敗,由於在運行時轉換髮生的時候類型實參是未知的。所以,這樣的轉換會致使編譯器發出unchecked cast的警告,例以下面這段程序:spa

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int>
            ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

fun main(args: Array<String>) {
    //(1) 正常運行。
    printSum(listOf(1, 2, 3))
    //(2) as 檢查是成功的,可是調用 intList.sum() 方法時會拋出異常。
    printSum(listOf("a", "b", "c"))
}
複製代碼

(2)調用時,並不會拋出IllegalArgumentException異常,而是在調用sum函數時才發生,由於sum函數試着從列表中讀取Number值而後把它們加在一塊兒,把String當作Number使用的嘗試會致使運行時的ClassCastExceptioncode

假如在編譯期,Kotlin已經知道了相應的類型信息,那麼is檢查是容許的:cdn

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>的檢查是可行的,由於咱們將函數類型的形參類型聲明爲了Collection<Int>,所以編譯期就肯定了集合包含的是整型數字。

不過,Kotlin有特殊的語法結構能夠容許你 在函數體中使用具體的類型實參,但只有inline函數能夠,接下來讓咱們來看看這個特性。

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

Kotlin泛型在運行時會被擦除,這意味着若是你有一個泛型類的實例,你沒法弄清楚在這個實例建立時用的到底是哪些類型實參。泛型函數的實參類型也是這樣,在調用泛型函數的時候,在函數體中你不能決定調用它用的類型實參。

//將會在編譯時拋出 "Cannot check for instance of erased type : T" 的異常
fun <T> isA(value : Any) = value is T
複製代碼

內聯函數的類型形參可以被實化

只有一種例外能夠避免這種限制:內聯函數。內聯函數的類型形參可以被實化,意味着你能夠 在運行時引用實際的類型實參。前面咱們介紹過內聯函數的兩個優勢:

  • 編譯器會把每一次函數調用都替換成函數實際的代碼實現
  • 若是該函數使用了lambdalambda的代碼也會內聯,因此不會建立匿名類

這裏,咱們介紹它一個新的優勢:對於泛型函數來講,它們的類型參數能夠被實化。咱們將方面的函數修改以下,聲明爲inline而且用reified標記類型參數,就能用該函數檢查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))
}
複製代碼

運行結果爲:

>> true
>> false
複製代碼

filterIsIntance函數能夠接收一個集合,選擇其中那些指定類的實例,而後返回這些被選中的實例:

fun main(args: Array<String>) {
    val items = listOf("one", 2, "three")
    println(items.filterIsInstance<String>())
}
複製代碼

運行結果爲:

[one, three]
複製代碼

該函數的簡化實現爲:

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

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

咱們之因此能夠在inline函數中使用element is T這樣的判斷,而不能在普通的類或函數中執行的緣由是由於:編譯器把 實現內聯函數的字節碼 插入每一次調用發生的地方,每次你 調用帶實化類型參數的函數 時,編譯器都知道此次特定調用中 用做類型實參的確切類型,所以,編譯器能夠生成 引用做爲類型實參的具體類 的字節碼。

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

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

另外一種實化類型參數的常見使用場景是接收java.lang.Class類型參數的API構建適配器。例如JDK中的ServiceLoader,它接收一個表明接口或抽象類的java.lang.Class,並返回實現了該接口的實例。

val serviceImpl = ServiceLoader.load(Service::class.java) 複製代碼

::class.java的語法展示瞭如何獲取java.lang.Class對應的Kotlin類,這和Java中的Service.Class是徹底等同的,如今咱們用 帶實化類型參數的函數 重寫這個例子:

val serviceImpl = loadService<String>()
複製代碼

loadService的定義爲以下,要加載的服務類 如今被指定成了loadService 函數的類型實參

inline fun <reified T> loadService() {
    //把 "T::class" 當成類型形參的類訪問。
    return ServiceLoader.load(T::class.java) } 複製代碼

這種用在普通類上的::class.java語法也能夠一樣用在實化類型參數上,使用這種語法會產生對應到指定爲類型參數的類的java.lang.Class,你能夠正常地使用它,最後咱們以一個startActivity的調用來結束本節的討論:

inline fun <reified T : Activity> Context.startActivity {
    val intent = new Intent(this, T::class.java) startActivity(intent) } >> startActivity<DetailActivity>() 複製代碼

2.4 實化類型參數的限制

咱們能夠按下面的方式來使用實化類型參數

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

不能作下面的事情:

  • 建立指定爲類型參數的類的實例
  • 調用類型參數類的伴生對象的方法
  • 調用 帶實化類型參數函數 的時候使用 非實化類型形參做爲類型實參
  • 把類、屬性或者非內聯函數的類型參數標記爲reified,由於實化類型參數只能用在內聯函數上,使用實化類型參數意味着函數和全部傳給它的lambda都會被內聯,若是內聯函數使用lambda的方法致使lambda不能被內聯,或者你不想lambda由於性能的關係被內聯,可使用noinline修飾符。

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

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

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

假設你有一個接受List<Any>做爲實參的函數,那麼把List<String>類型的變量傳遞給這個函數是否安全呢?咱們來看下面兩個例子:

  • 第一個例子
fun printContents(list: List<Any>) {
    println(list.joinToString())
}

fun main(args: Array<String>) {
    printContents(listOf("abc", "bac"))
}
複製代碼

這上面的函數能夠正常地工做,函數把每一個元素都看成Any對待,並且由於每一個字符都是Any,所以這是徹底安全的,運行結果爲:

>> 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>的函數,編譯器將不會經過調用。

所以,當咱們將一個字符串列表傳遞給指望Any對象的列表時,若是 函數添加或者替換了 列表中的元素(經過MutableList來推斷)就是不安全的,由於這樣會產生類型不一致的可能,不然它就是安全的。

3.2 類、類型和子類型

變量的類型 規定了 變量的可能值,有時候咱們會把類型和類當成一樣的概念使用,但它們不同。

類、類型

非泛型類

對於非泛型類來講,類的名稱能夠直接看成類型使用。例如,var x : String聲明瞭一個能夠保存String類的實例的變量,而var x : String?聲明瞭它的可空類型版本,這意味着 一個Kotlin類均可以用於構造至少兩種類型

泛型類

要獲得一個合法的類型,須要首先獲得一個泛型類,並用一個做爲 類型實參的具體類型 替換泛型類的 類型形參

List是一個類而不是類型,下面列舉出來的全部替代品都是合法的類型:List<Int>List<String?>List<List<String>>,每個 泛型類均可能生成潛在的無限數量的類型

子類型

子類型的含義爲:

任什麼時候候若是須要的是類型A的值,可以使用類型B的值當作A的值,類型B就稱爲類型A的子類型。

例如IntNumber的子類型,但Int不是String的子類型,這個定義還代表了任何類型均可以被認爲是它本身的子類型。

超類型

超類型子類型 的反義詞

若是AB的子類型,那麼B就是A的超類型。

編譯器在每一次給變量賦值或者給函數傳遞實參的時候都要作這項檢查:

  • 只有 值的類型變量類型的子類型 時,才容許存儲變量的值
  • 只有當 表達式的類型函數參數的類型的子類型 時,才容許把該表達式傳給函數

子類、子類型

在簡單狀況下,子類和子類型本質上是同樣的,例如Int類是Number的子類,所以Int類型是Number類型的子類型。

一個非空類型是它的可空版本的子類型,但它們都對應着同一個類,你始終可以在可空類型的變量中存儲非空類型值。

當開始涉及泛型類時,子類型和子類之間的差別就顯得格外重要。正如咱們上面見到的,MutableList<String>不是MutableList<Any>的子類型。

對於泛型類MutableList而言,不管AB是什麼關係,MutableList<A>既不是MutableList<B>的子類型也不是它的超類型,它就被稱爲 在該類型參數上是不變型的

Java中的全部類都是不變型的。在前一節中,咱們見到了List類,對它來講,子類型化規則不同,Kotlin中的List接口表示的是隻讀集合。若是AB的子類型,那麼List<A>就是List<B>的子類型,這樣的類或者接口被稱爲 協變的

3.3 協變:保留子類型化關係

一個協變類是一個泛型類,若是AB的子類型,那麼Producer<A>就是Producer<B>的子類型,咱們說 子類型化被保留了

Kotlin中,要聲明類在某個類型參數上是能夠協變的,在該類型參數的名稱前加上out關鍵字便可,下面例子就能夠表達爲:Producer類在類型參數T上是能夠協變的。

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

將一個類的類型參數標記爲協變的,在 該類型實參沒有精確匹配到函數中定義的類型形參時,可讓該類的值做爲這些函數的實參傳遞,也能夠做爲這些函數的返回值

你不能把任何類都變成協變的,這樣不安全。讓類在某個類型參數變爲協變,限制了該類中對該類型參數使用 的可能性,要保證類型安全,你只能用在所謂的out位置,意味着這個類 只能生產類型T的值而不能消費它們

在類成員的聲明中類型參數的使用分爲inout位置,考慮這樣一個類,它聲明瞭一個類型參數T幷包含了一個使用T的函數:

  • 若是函數把T當成返回類型,咱們說它在out位置,這種狀況下,該函數生產類型爲T的值
  • 若是T用做函數參數的類型,它就在in的位置,這樣函數消費類型爲T的值。

所以類型參數T上的關鍵字有兩層含義:

  • 子類型化會被保留,即前面談到的Producer<Cat>Producer<Animal>的子類型
  • T只能用在out位置

在構造方法的參數上使用 out

構造方法的參數既不在in位置,也再也不out位置,即便類型參數聲明成了out,仍然能夠在構造方法參數的聲明中使用它。

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

若是把類的實例當成一個更泛化的類型的實例使用,變型會防止該實例被誤用,不能調用存在潛在危險的方法。構造方法不是那種在實例建立以後還能調用的方法,所以它不會有潛在的危險。

然而,若是你在構造方法的參數上使用了關鍵字varval,同時就會聲明一個gettersetter,所以,對只讀屬性來講,類型參數用在了out位置,而可變屬性在outin位置都使用了它。

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

上面這個例子中,T不能用out標記,由於類包含屬性leadAnimalsetter,它在in位置用到了T

位置規則只覆蓋了類外部可見的 API

位置規則只覆蓋了類外部可見的api,私有方法的參數既不在in位置,也不在out位置,變型規則只會防止外部使用者對類的誤用,但不會對類本身的實現起做用。

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

如今能夠安全地讓HerdT上協變,由於屬性leadAnimal被聲明成了私有。

3.4 逆變:反轉子類型化關係

逆變的概念能夠當作是協變的鏡像,對一個逆變類來講,它的子類型化關係與用做類型實參的類的子類型化關係是相反的:若是BA的子類型,那麼Consumer<A>就是Consumer<B>的子類型。

Comparator接口爲例,這個接口定義了一個compare方法,用於比較兩個指定的對象:

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

這個接口方法只是消費類型爲T的值,這說明T只在in位置使用,所以它的聲明以前用了in關鍵字。

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

val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
val strings : List<String> = ...
strings.sortedWith(anyComparator)
複製代碼

sortedWith指望一個Comparator<String>,傳給它一個能比較更通常的類型的比較器是安全的。若是你要在特定類型的對象上執行比較,可使用能處理該類型或者它的超類型的比較器。

這說明Comparator<Any>Comparator<String>的子類型,其中AnyString的超類型。不一樣類型之間的子類型關係這些類型的比較器之間的子類型關係 截然相反。

in關鍵字的意思是,對應類型的值是傳遞進來給這個類的方法的,而且被這些方法消費。和協變的狀況相似,約束類型參數的使用將致使特定的子類型化關係。

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

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

這意味着對這個函數類型的第一類型參數來講,子類型化反轉了,而對於第二個類型參數來講,子類型化保留了。例如,你有一個高階函數,該函數嘗試對你全部的貓進行迭代,你能夠把一個接收動物的lambda傳遞給它。

fun enumerate(f : (Cat) -> Number) { ... }
fun Animal.getIndex() : Int = ...

>> enumerate(Animal :: getIndex)
複製代碼

更多文章,歡迎訪問個人 Android 知識梳理系列:

相關文章
相關標籤/搜索