變型:描述擁有相同基礎類型和不一樣類型實參的(泛型)類型之間是如何關聯的,例如,List和List之間如何關聯。安全
子類型:任什麼時候候若是須要的是類型A的值,都可以使用類型B的值(看成A的值),類型B就稱爲類型A的子類型。markdown
超類型:是子類型的反義詞。若是A是B的子類型,那麼B就是A的超類型。函數
fun test1(i: Int) {
// 編譯經過,由於Int是Number的子類型
val n: Number = i
fun f(s: String) { println(s) }
// 不能編譯,由於Int 不是String的子類型
f(i)
}
複製代碼
一個非空類型是它的可空版本的子類型,但它們都對應着同一個類。你始終能在可空類型的變量中存儲非空類型的值,但反過來卻不行(null不是非空類型的變量能夠接受的值)。oop
一個協變類是一個泛型類(以Producer爲例),對這種類來講,下面的描述是成立的:若是A是B的子類型,那麼Producer就是Producer的子類型。咱們說子類型被保留了。ui
在Kotlin中,要聲明類在某個類型參數上是能夠協變的,在該類型參數的名稱前加上out關鍵字便可:spa
// 類被聲明成在T上協變
interface Producer<out T> {
fun produce(): T
}
複製代碼
open class Animal {
fun feed() { ... }
}
// 類型參數沒有聲明成協變的
class Herd<T: Animal> {
val size: Int get() = ...
operator fun get(i: Int): T { ... }
}
複製代碼
// Cat是一個Animal
class Cat: Animal() {
fun cleanLitter() { ... }
}
fun takeCareOfCats(cats: Herd<Cat>) {
for(i in 0 until cats.size) {
cats[i].cleanLitter()
// 錯誤:推導的類型是Herd<Cat>,但指望的倒是Herd<Animal>
// feedAll(cats)
}
}
複製代碼
若是嘗試把貓羣傳給feedAll()函數,在編譯期會獲得類型不匹配的錯誤。由於Herd
類中的類型參數T
沒有任何變型修飾符,貓羣不是畜羣的子類。code
於是可使用協變來解決。orm
// 類型參數T如今是協變的
class Herd<out T: Animal> {
...
}
複製代碼
不能把任何類都變成協變的:這樣不安全。讓類在某個類型參數變成協變,限制了該類中對該參數使用的可能性。接口
圖1中,類的類型參數前
的out
關鍵字要求全部使用T的方法只能把T放在out
位置而不能放在in位置。這個關鍵字約束了使用T的可能性,這保證了對應子類型關係的安全性。get
class Herd<out T: Animal> {
val size: Int get() = ...
// 把T做爲返回類型使用
operator fun get(i: Int): T { ... }
}
複製代碼
這裏一個out位置,能夠安全地把類聲明成協變的。
重申一下,類型參數T上的關鍵字out有兩層含義:
T
只能用在out
位置類型參數不光能夠直接看成參數類型或者返回類型使用,還能夠看成另外一個類型的類型實參。
注意:構造方法的參數既不在
in
位置,也不在out
位置
位置規則只覆蓋了類外部可見的(public
、protected
和internal
)API。私有方法的參數既不在in位置也不在out位置。
變型規則只會防止外部使用者對類的誤用但不會對類本身的實現起做用:
class Herd<out T: Animal>(private var leadAnimal: T, varage animals: T) { ... }
複製代碼
逆變的概念能夠被當作協變的鏡像:對一個逆變類來講,它的子類型化關係與用做類型實參的類的子類型化關係是相反的。
interface Comparator<in T> {
// 在「in」位置使用T
fun compare(e1: T, e2: T): Int { ... }
}
複製代碼
這個接口方法只是消費類型爲T的值。這說明T
只在in
位置使用,所以它的聲明以前用了in關鍵字。
in
關鍵字的含義是:對應類型的值是傳遞進來給這個類的方法的,而且被這些方法消費。
表1 協變的、逆變的和不變型的表
協變 | 逆變 | 不變型 |
---|---|---|
Producer | Consumer | MutableList |
類的子類型化保留了:Producer是Producer的子類型 | 子類型化反轉了:Consumer 是 Consumer的子類型 | 沒有子類型化 |
T只能在out位置 | T只能在in位置 | T能夠在任何位置 |
**聲明點變型:**在類聲明時就可以制定變型修飾符是很方便的,由於這些修飾符會應用到全部類被使用的地方。
在Java
中,每一次使用帶類型參數的類型的時候,還能夠制定這個類型參數是否能夠用它的子類型或者超類型替換,這叫做使用點變型。
// 能夠給類型的用法加上「out」關鍵字:沒有使用那些T用在「in」位置的方法
fun <T> copyDataByOut(source: MutableList<out T>, destination: MutableList<T>) {
for(item in source) {
destination.add(item)
}
}
複製代碼
// 容許目標元素的類型是來源元素類型的超類型
fun <T> copyDataByIn(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>
。
星號投影語法,代表不知道關於泛型實參的任何信息。
星號投影的語義:
MutableList<*>
和MutableList<Any?>
** 不同(這裏很是重要的是MutableList<T>
在T上是不可變的)。MutableList<*>
這種列表包含的是任何類型的元素。MutableList<*>
是包含某種特定類型元素的列表使用星號投影的語法:不須要使用任何在簽名中引用類型參數的方法,或者只是讀取數據而不關心它的具體類型。
// 每一種列表都是可能的實參
fun printFirst(list: List<*>) {
// isNotEmpty()沒有使用泛型類型參數
if(list.isNotEmpty())
// first()如今返回的是Any?,可是這裏足夠了
println(list.first())
}
fun printFirstTest() {
println(printFirst(listOf("Svetlana", "Dmitry")))
}
複製代碼
在使用點類型的狀況下,有一個替代方案——引入一個泛型類型參數:
// 再一次,每一種列表都是可能的實參
fun <T> printFirstByT(list: List<T>) {
if(list.isNotEmpty()) {
// first()如今返回的實T的值
println(list.first())
}
}
複製代碼
星號投影的語法很簡潔,但只能用在對泛型類型實參的確切值不感興趣的地方:只是使用生產值的方法,並且不關心那些值的類型。