Kotlin 知识梳理(13) 运行时的泛型
一、本文概要
本文是對<<Kotlin in Action>>的學習筆記,如果需要運行相應的代碼可以訪問在線環境 try.kotlinlang.org,這部分的思維導圖為:
二、運行時的泛型:擦除和實化類型參數
2.1 運行時的泛型
和Java一樣,Kotlin的泛型在運行時也被擦除了,這意味著 泛型類實例不會攜帶用于創建它的類型實參的信息。
例如,如果你創建了一個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<*>) 復制代碼實際上,泛型類型擁有的每個類型形參都需要一個*,現在你可以認為它就是 擁有未知類型實參的泛型類型。
在as和as?轉換中仍然可以使用一般的泛型類型,但是如果該類 有正確的基礎類型但類型實參是錯誤的,轉換也不會失敗,因為在運行時轉換發生的時候類型實參是未知的。因此,這樣的轉換會導致編譯器發出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) 正常運行。printSum(listOf(1, 2, 3))//(2) as 檢查是成功的,但是調用 intList.sum() 方法時會拋出異常。printSum(listOf("a", "b", "c")) } 復制代碼在(2)調用時,并不會拋出IllegalArgumentException異常,而是在調用sum函數時才發生,因為sum函數試著從列表中讀取Number值然后把它們加在一起,把String當做Number使用的嘗試會導致運行時的ClassCastException。
假如在編譯期,Kotlin已經知道了相應的類型信息,那么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>的檢查是可行的,因為我們將函數類型的形參類型聲明為了Collection<Int>,因此編譯期就確定了集合包含的是整型數字。
不過,Kotlin有特殊的語法結構可以允許你 在函數體中使用具體的類型實參,但只有inline函數可以,接下來讓我們來看看這個特性。
2.2 聲明帶實化類型參數的函數
Kotlin泛型在運行時會被擦除,這意味著如果你有一個泛型類的實例,你無法弄清楚在這個實例創建時用的究竟是哪些類型實參。泛型函數的實參類型也是這樣,在調用泛型函數的時候,在函數體中你不能決定調用它用的類型實參。
//將會在編譯時拋出 "Cannot check for instance of erased type : T" 的異常 fun <T> isA(value : Any) = value is T 復制代碼內聯函數的類型形參能夠被實化
只有一種例外可以避免這種限制:內聯函數。內聯函數的類型形參能夠被實化,意味著你可以 在運行時引用實際的類型實參。前面我們介紹過內聯函數的兩個優點:
- 編譯器會把每一次函數調用都替換成函數實際的代碼實現
- 如果該函數使用了lambda,lambda的代碼也會內聯,所以不會創建匿名類
這里,我們介紹它一個新的優點:對于泛型函數來說,它們的類型參數可以被實化。我們將方面的函數修改如下,聲明為inline并且用reified標記類型參數,就能用該函數檢查value是不是T的實例:
inline fun <reified T> isA(value: Any) = value is Tfun 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、!is、as、as?
- 使用Kotlin反射API,::class
- 獲取對應的java.lang.Class,::class.java
- 作為調用其它函數的類型實參
不能做下面的事情:
- 創建指定為類型參數的類的實例
- 調用類型參數類的伴生對象的方法
- 調用 帶實化類型參數函數 的時候使用 非實化類型形參作為類型實參
- 把類、屬性或者非內聯函數的類型參數標記為reified,因為實化類型參數只能用在內聯函數上,使用實化類型參數意味著函數和所有傳給它的lambda都會被內聯,如果內聯函數使用lambda的方法導致lambda不能被內聯,或者你不想lambda因為性能的關系被內聯,可以使用noinline修飾符。
三、變型:泛型和子類型化
變型的概念描述了擁有 相同基礎類型 和 不同類型實參 的類型之間是如何關聯的,例如List<String>和List<Any>之間如何關聯。
3.1 為什么存在變型:給函數傳遞實參
假設你有一個接受List<Any>作為實參的函數,那么把List<String>類型的變量傳遞給這個函數是否安全呢?我們來看下面兩個例子:
- 第一個例子:
這上面的函數可以正常地工作,函數把每個元素都當作Any對待,而且因為每個字符都是Any,因此這是完全安全的,運行結果為:
>> abc, bac 復制代碼- 第二個例子,與之前不同,它會修改列表:
這里聲明了一個類型為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的子類型。
例如Int是Number的子類型,但Int不是String的子類型,這個定義還表明了任何類型都可以被認為是它自己的子類型。
超類型
超類型 是 子類型 的反義詞
如果A是B的子類型,那么B就是A的超類型。
編譯器在每一次給變量賦值或者給函數傳遞實參的時候都要做這項檢查:
- 只有 值的類型 是 變量類型的子類型 時,才允許存儲變量的值
- 只有當 表達式的類型 是 函數參數的類型的子類型 時,才允許把該表達式傳給函數
子類、子類型
在簡單情況下,子類和子類型本質上是一樣的,例如Int類是Number的子類,因此Int類型是Number類型的子類型。
一個非空類型是它的可空版本的子類型,但它們都對應著同一個類,你始終能夠在可空類型的變量中存儲非空類型值。
當開始涉及泛型類時,子類型和子類之間的差異就顯得格外重要。正如我們上面見到的,MutableList<String>不是MutableList<Any>的子類型。
對于泛型類MutableList而言,無論A和B是什么關系,MutableList<A>既不是MutableList<B>的子類型也不是它的超類型,它就被稱為 在該類型參數上是不變型的。
Java中的所有類都是不變型的。在前一節中,我們見到了List類,對它來說,子類型化規則不一樣,Kotlin中的List接口表示的是只讀集合。如果A是B的子類型,那么List<A>就是List<B>的子類型,這樣的類或者接口被稱為 協變的。
3.3 協變:保留子類型化關系
一個協變類是一個泛型類,如果A是B的子類型,那么Producer<A>就是Producer<B>的子類型,我們說 子類型化被保留了。
在Kotlin中,要聲明類在某個類型參數上是可以協變的,在該類型參數的名稱前加上out關鍵字即可,下面例子就可以表達為:Producer類在類型參數T上是可以協變的。
interface Producer<out T> {fun produce() : T } 復制代碼將一個類的類型參數標記為協變的,在 該類型實參沒有精確匹配到函數中定義的類型形參時,可以讓該類的值作為這些函數的實參傳遞,也可以作為這些函數的返回值。
你不能把任何類都變成協變的,這樣不安全。讓類在某個類型參數變為協變,限制了該類中對該類型參數使用 的可能性,要保證類型安全,你只能用在所謂的out位置,意味著這個類 只能生產類型T的值而不能消費它們。
在類成員的聲明中類型參數的使用分為in和out位置,考慮這樣一個類,它聲明了一個類型參數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) { ... } 復制代碼如果把類的實例當成一個更泛化的類型的實例使用,變型會防止該實例被誤用,不能調用存在潛在危險的方法。構造方法不是那種在實例創建之后還能調用的方法,因此它不會有潛在的危險。
然而,如果你在構造方法的參數上使用了關鍵字var和val,同時就會聲明一個getter和setter,因此,對只讀屬性來說,類型參數用在了out位置,而可變屬性在out和in位置都使用了它。
class Herd<T : Animal> (var leadAnimal : T, vararg animals : T) { ... } 復制代碼上面這個例子中,T不能用out標記,因為類包含屬性leadAnimal的setter,它在in位置用到了T。
位置規則只覆蓋了類外部可見的 API
位置規則只覆蓋了類外部可見的api,私有方法的參數既不在in位置,也不在out位置,變型規則只會防止外部使用者對類的誤用,但不會對類自己的實現起作用。
class Herd<out T : Animal> (private var leadAnimal : T, vararg animals : T) { ... } 復制代碼現在可以安全地讓Herd在T上協變,因為屬性leadAnimal被聲明成了私有。
3.4 逆變:反轉子類型化關系
逆變的概念可以看成是協變的鏡像,對一個逆變類來說,它的子類型化關系與用作類型實參的類的子類型化關系是相反的:如果B是A的子類型,那么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>的子類型,其中Any是String的超類型。不同類型之間的子類型關系 和 這些類型的比較器之間的子類型關系 截然相反。
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 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/
總結
以上是生活随笔為你收集整理的Kotlin 知识梳理(13) 运行时的泛型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Android]Space控件的应用场
- 下一篇: 阿里云服务器tomcat配置https