Kotlin核心語法(七):泛型

博客主頁java

1. 泛型類型參數

1.1 泛型函數和屬性

在使用集合的庫函數都是泛型的。咱們來看下slice函數的定義:segmentfault

//public fun 類型形參聲明 List<接收者類型形參>.slice(indices: Iterable<Int>): List<返回類型的類型形參> 
public fun <T> List<T>.slice(indices: Iterable<Int>): List<T>

在一個具體的列表上調用這個函數時,能夠顯式地指定類型實參,但大部分狀況下沒必要這樣作,由於編譯器會推導出類型。api

val letters = ('a'..'z').toList()

//顯式地指定類型實參
println(letters.slice<Char>(0..2))
// [a, b, c]

// 編譯器推導出這裏的T是Char
println(letters.slice(10..13))
// [k, l, m, n]

先來看下filter函數的聲明,它接收一個函數類型:(T) -> Boolean的參數安全

val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf<String>("Bob", "Svetlana")

// filter函數的聲明
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> 

readers.filter { it !in authors }

編譯器推斷T就是String,由於它知道函數應該在List<T> 上調用,而它的接收者readers的真實類型是List<String>ide

能夠給類或者接口的方法、頂層函數,以及擴展函數聲明類型參數。還能夠聲明泛型的擴展屬性:函數

// 這個泛型擴展屬性能在任何類型的元素列表上調用
val <T> List<T>.penultimate: T
    get() = this[size - 2]

// 類型T會被推導爲Int
println(listOf(1, 2, 3, 4).penultimate)
// 3

1.2 聲明泛型類

與java同樣,在kotlin中也是經過在類名稱後加上一對尖括號,並把類型參數放在尖括號內來聲明泛型類及泛型接口,這樣就能夠在類的主體內像其它類型同樣使用泛型參數。性能

// List接口定義了類型參數 E
public interface List<out E> {
   // 在類或者接口內部,E 能夠看成普通的類型使用
  public operator fun get(index: Int): E
  // ...
}

若是一個類繼承了泛型類(或者實現了泛型接口),就等爲基礎類型的泛型形參提供一個類型實參,它能夠是具體的類型或者一個類型形參:this

// 這個類實現了List,並提供了具體類型實參 String
class StringList : List<String> {
    override fun get(index: Int): String  = ...
}

// ArrayList的泛型類型形參 T 就是List的類型實參
class ArrayList<T> : List<T> {
    override fun get(index: Int): T  = ...
}

StringList類被聲明只能包含String元素,而類ArrayList指定了本身的類型參數T並指定爲父類的類型實參。spa

一個類還能夠把它本身做爲類型實參引用。code

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

public class String : Comparable<String> {
    public override fun compareTo(other: String): Int
}

String類實現了Comparable泛型接口,提供類型String給類型實參T。

1.3 類型參數約束

類型參數約束能夠限制做爲(泛型)類和(泛型)函數的類型實參的類型。

若是把一個類型指定爲泛型類型形參的上界約束,在泛型類型具體的初始化中,其對應的類型實參就必須是這個具體類型或者它的子類型。
在java中,使用extends關鍵字:

<T extends Number> T sum(List<T> list)

在kotlin中,使用冒號(:)

//  經過在類型參數後指定上界來定義約束

// <類型參數 : 上界>
fun <T : Number> List<T>.sum(): T

若是指定了類型形參T的上界,就能夠把類型T的值看成它的上界(類型)的值使用:

// 指定Number爲類型形參的上界
fun <T : Number> oneHalf(value: T): Double {
    // 調用Number類中的方法
    return value.toDouble() / 2.0f
}

println(oneHalf(3))
// 1.5

再來看一個例子:T的上界是泛型類型Comparable<T>,String類繼承了Comparable<String>,String就能夠做爲max函數的有效類型實參。

// 聲明帶類型參數約束的函數
fun <T : Comparable<T>> max(first: T, second: T): T {
    // 根據kotlin運算符約定會被編譯成first.compareTo(second) > 0
    return if (first > second) first else second
}

println(max("kotlin", "java"))
// kotlin

1.4 讓類型形參非空

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

class Processor<T> {
    fun process(value: T) {
        // "value"是可空的,因此要用安全調用
        value?.hashCode()
    }
}

// 可空類型String?被用來替換T
val nullableStringProcessor = Processor<String?>()
// 使用null做爲value實參的代碼能夠編譯
nullableStringProcessor.process(null)

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

// 指定非空上界
class Processor<T : Any> {
    fun process(value: T) {
        // 類型T的值如今是非空的
        value.hashCode()
    }
}

約束<T : Any>確保了類型T永遠都是非空類型。

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

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

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

與java同樣,kotlin的泛型在運行時也被擦除了。例如:建立一個List<String>,在運行時只能看到它是一個List。

注意一點:擦除泛型類型信息是有好處的,應用程序使用的內存總量較小,由於要保存在內存中的類信息更少。

可使用特殊的星號投影語法來檢查,一個值是不是列表,而不是set或者其餘對象。

if (value is List<*>) { ... }

能夠認爲它就是擁有未知類型實參的泛型類型(或者類比於java的List<?>)。

as和as?轉換中可使用通常的泛型類型。若是該類有正確的基礎類型但類型實參是錯誤的,轉換也不會失敗,由於在運行時轉換髮生的時候類型實參是未知的,這樣寫,編譯器會發出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

printSum(listOf("a", "b", "c"))
// java.lang.ClassCastException: 
// java.lang.String cannot be cast to java.lang.Number

若是傳一個錯誤類型的值,運行時就會拋出ClassCastException異常。

// 對已知類型實參作類型轉換
fun printSum(c: Collection<Int>) {
   if (c is List<Int>) {
       println(c.sum())
   }
}

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

kotlin泛型在運行時會被擦除。在調用泛型函數的時候,在函數體中不能決定調用它用的類型實參:

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

只有一種例外能夠避免這種限制:內聯函數。內聯函數的類型形參可以被實化,意味着能夠在運行時引用實際的類型實參。

若是把isA函數聲明成inline並用reified標記類型參數,就能夠用該函數檢查value是否是T實例:

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

// 如今代碼能夠編譯了
inline fun <reified T> isA(value: Any) = value is T

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

// 使用標準庫函數filterIsInstance

val list = listOf("one", 2, "three")
println(list.filterIsInstance<String>())
// [one, three]

經過指定<String>做爲函數的類型實參,代表只關心字符串,因此函數的返回類型是List<String>
這種狀況下,類型實參在運行時是已知的,函數filterIsInstance使用它來檢查列表中的值是否是指定爲該類型實參的類的實例。

// filterIsInstance函數的定義

// 「reified」聲明瞭類型參數不會在運行時被擦除
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
    return filterIsInstanceTo(ArrayList<R>())
}

public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
    // 能夠檢查元素是否是指定爲類型實參的類的實例
    for (element in this) if (element is R) destination.add(element)
    return destination
}

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

另外一種實化類型參數的常見場景是爲接收java.lang.Class類型參數的API構建適配器。

如:jdk中的ServiceLoader,它接收一個表明接口或者抽象類的java.lang.Class,並返回實現了該接口(或者繼承了該抽象類)的類的實例。

// 使用標準的ServiceLoader java api加載一個服務

val serviceImpl = ServiceLoader.load(Service::class.java)

::class.java獲取java.lang.Class對應的kotlin類,這和java中的Service.class是徹底等同的。

接下來用帶實化類型參數的函數重寫這個例子:

// loadService函數定義
// 類型參數標記成了reified
inline fun <reified T> loadService(): ServiceLoader<T> {
    // 把T::class當成類型形參的類訪問
    return ServiceLoader.load(T::class.java)
}

val serviceImpl = loadService<Service>()

能夠簡化Android上的startActivity函數:

// 可使用實化類型參數來代替傳遞做爲java.lang.Class的activity類
inline fun <reified T : Activity> Context.startActivity() {
   // 把T:class當成類型參數的類訪問
   val intent = Intent(this, T:class.java)
   startActivity(intent)
}

// 調用方法
startActivity<DetailActivity>()

2.4 實化類型參數的限制

能夠按照下面的方式使用實化類型參數:

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

不能作下面這些事情:

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

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

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

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

假設有一個接收List<Any>做爲實參的函數,把List<String>類型的變量傳給這個函數是否安全?
若是函數添加或者替換了列表中的元素是不安全的,由於這樣會產生類型不一致的可能性,不然它就是安全的。

下面這段代碼是安全的,由於String類繼承了Any,因此是安全的。

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

printContents(listOf("abc", "cbd"))
// abc, cbd

看另外一個函數,它修改列表(接收一個MutableList做爲參數):

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

而後把一個字符串列表傳給這個函數,將發生什麼呢?

val list = mutableListOf("abc", "cbd")
// type mismatch,編譯通不過
addAnswer(list)

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

若是A是B的子類型,那麼Producer<A>就是Producer<B>的子類型。咱們說子類型化被保留了

在kotlin中,要聲明類在某個類型參數上是能夠協變的,在該類型參數的名稱前加上 out 關鍵字便可:

// 類被聲明成在T上協變
interface Producer<out T> {
    fun produce(): T
}

類型參數 T 上的關鍵宇 out 有兩層含義:

  • 子類型化會被保留

Producer<Cat>Producer<Animal> 的子類型

  • T只能用在 out 位置

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

// kotlin標準庫中List的定義
public interface List<out E> : Collection<E> {
    // 只讀接口只定義了返回 E 的方法。因此E在 」out「 位置
    public operator fun get(index: Int): E
    // .... 
}

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

public interface List<out E> : Collection<E> {
     // 這裏的 E 也在 」out「位置
     public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

不能把MutableList<E>在它的類型參數上聲明成協變的,由於它既含有接收類型爲E的值做爲參數的方法,也含有返回這種值的方法(所以,E 出現 in 和 out 兩種位置上)。

// MutableList不能在E上聲明成協變的
public interface MutableList<E> : List<E>, MutableCollection<E> {
    // 由於E用在了 「in」 位置
    override fun add(element: E): Boolean
}

其中構造方法的參數既不在 in 位置,也不在 out 位置。

若是你在構造方法的參數上使用了關鍵字 val 和 var,同時就會聲明一個getter 和setter(若是屬性是可變的)。所以,對只讀屬性來講,類型參數用在了 out 位置,而可變屬性在 out 位置 in 位置都使用了它。

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

上面這個例子中, T 不能用 out 標記,由於類包含屬性 leadAnimal 的setter ,它在 in 位置用到了 T。注意:私有方法的參數既不在 in 位置也不在 out 位置。若是leadAnimal是私有的,可使用協變。

3.4 逆變:反轉子類型關係

看下Comparator接口:

interface Comparator<in T> { 
   // 在 「in」 位置使用了 T
   fun compare(el: T, e2: T): Int { ... }
}

在類型參數T上的in關鍵字意味着子類型化被反轉了,並且 T 只能用在 in 位置。

一個類能夠在一個類型參數上協變,同時在另一個類型參數上逆變。

interface Functionl<in P, out R> { 
    operator fun invoke (p: P) : R
}

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索