本文是對<<Kotlin in Action>>
的學習筆記,若是須要運行相應的代碼能夠訪問在線環境 try.kotlinlang.org,這部分的思惟導圖爲: java
和
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
在as
和as?
轉換中仍然可使用通常的泛型類型,可是若是該類 有正確的基礎類型但類型實參是錯誤的,轉換也不會失敗,由於在運行時轉換髮生的時候類型實參是未知的。所以,這樣的轉換會致使編譯器發出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
使用的嘗試會致使運行時的ClassCastException
。code
假如在編譯期,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
函數能夠,接下來讓咱們來看看這個特性。
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 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
那樣普通的方式調用。
另外一種實化類型參數的常見使用場景是接收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>() 複製代碼
咱們能夠按下面的方式來使用實化類型參數
is
、!is
、as
、as?
Kotlin
反射API
,::class
java.lang.Class
,::class.java
不能作下面的事情:
reified
,由於實化類型參數只能用在內聯函數上,使用實化類型參數意味着函數和全部傳給它的lambda
都會被內聯,若是內聯函數使用lambda
的方法致使lambda
不能被內聯,或者你不想lambda
由於性能的關係被內聯,可使用noinline
修飾符。變型的概念描述了擁有 相同基礎類型 和 不一樣類型實參 的類型之間是如何關聯的,例如List<String>
和List<Any>
之間如何關聯。
假設你有一個接受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
來推斷)就是不安全的,由於這樣會產生類型不一致的可能,不然它就是安全的。
變量的類型 規定了 變量的可能值,有時候咱們會把類型和類當成一樣的概念使用,但它們不同。
對於非泛型類來講,類的名稱能夠直接看成類型使用。例如,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>
的子類型,這樣的類或者接口被稱爲 協變的。
一個協變類是一個泛型類,若是
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
位置構造方法的參數既不在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
,私有方法的參數既不在in
位置,也不在out
位置,變型規則只會防止外部使用者對類的誤用,但不會對類本身的實現起做用。
class Herd<out T : Animal> (private var leadAnimal : T, vararg animals : T) { ... }
複製代碼
如今能夠安全地讓Herd
在T
上協變,由於屬性leadAnimal
被聲明成了私有。
逆變的概念能夠當作是協變的鏡像,對一個逆變類來講,它的子類型化關係與用做類型實參的類的子類型化關係是相反的:若是
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)
複製代碼