8.4 Kotlin泛型

泛型,即 "參數(shù)化類型",將類型參數(shù)化,可以用在類、接口、方法上。
與Java語言中的非常相似,但是Kotlin語言的創(chuàng)建者試圖通過引入特殊的關(guān)鍵字(如out和in)來使它們更加直觀和易于理解。
以下是使用泛型的主要優(yōu)點:

  • 類型安全:通用允許僅保留單一類型的對象。泛型不允許存儲其他對象。
  • 不需要類型轉(zhuǎn)換:不需要對對象進(jìn)行類型轉(zhuǎn)換。
  • 編譯時間檢查:在編譯時檢查泛型代碼,以便在運行時避免任何問題

泛型類

像 java 一樣,Kotlin 中的類可以擁有類型參數(shù):

class Box<T>(t: T){
    var value = t
}

通常來說,創(chuàng)建一個這樣類的實例,我們需要提供類型參數(shù):

val box: Box<Int> = Box<Int>(1)

但如果類型有可能是推斷的,比如來自構(gòu)造函數(shù)的參數(shù)或者通過其它的一些方式,一個可以忽略類型的參數(shù):

val box = Box(1)    //1是 Int 型,因此編譯器會推導(dǎo)出我們調(diào)用的是 Box<Int>

泛型接口

聲明泛型接口的格式與聲明泛型類相似,定義如下泛型接口。

interface GenericsInterface<T> {
    public T generate();
}

在實現(xiàn)泛型接口的類中,指定泛型實參。

class GenericsClass implements GenericsInterface<String> {
    public String generate() {
        return "hello";
    }
}

泛型函數(shù)

范型函數(shù)
函數(shù)也可以像類一樣有類型參數(shù)。類型參數(shù)在函數(shù)名之前:

fun <T> singletonList(item: T): List<T> {
    // ...
}

fun <T> T.basicToString() : String {  // extension function
    // ...
}

調(diào)用范型函數(shù)需要在函數(shù)名后面制定類型參數(shù):

val l = singletonList<Int>(1)

范型約束

指定類型參數(shù)代替的類型集合可以用通過范型約束進(jìn)行限制。
上界(upper bound):最常用的類型約束是上界,在 Java 中對應(yīng) extends關(guān)鍵字:

fun <T : Comparable<T>> sort(list: List<T>) {
    // ...
}

冒號后面指定的類型就是上界:只有 Comparable<T>的子類型才可以取代 T 比如:

sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>

默認(rèn)的上界是 Any?。在尖括號內(nèi)只能指定一個上界。如果要指定多種上界,需要用 where 語句指定:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable,
          T : Cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}

聲明處變型*

java聲明處變型

假如有個范型接口Source<T>,沒有任何接收 T 作為參數(shù)的方法,唯一的方法就是返回 T:

// Java
interface Source<T> {
  T nextT();
}

存儲一個Source<String>的實例引用給一個類型為 Source<Object> 是十分安全的。但 Java并不知道,而且依然禁止這么做:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!! Not allowed in Java
  // ...
}

為此,我們不得不聲明對象類型為 Source<? extends Object>,這樣做并沒有太大的意義,因為我們可以像以前一樣調(diào)用所有方法,因此并沒有通過復(fù)雜的類型添加什么值。但編譯器不知道。

Kotlin聲明處變型

在 Kotlin 中,有種可以將這些東西解釋給編譯器的辦法,叫做聲明處變型:通過注解類型參數(shù) T 的來源,來確保它僅從 Source<T> 成員中返回(生產(chǎn)),并從不被消費。 為此,我們提供 out 修飾符:

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

一般原則是:當(dāng)一個類 C 的類型參數(shù) T 被聲明為 out 時,它就只能出現(xiàn)在 C 的成員的輸出-位置,結(jié)果是 C<Base> 可以安全地作為 C<Derived>的超類。

更聰明的說法就是,當(dāng)類 C 在類型參數(shù) T 之下是協(xié)變的,或者 T 是一個協(xié)變類型??梢园?C 想象成 T 的生產(chǎn)者,而不是 T 的消費者。

out 修飾符本來被稱之為變型注解,但由于同處與類型參數(shù)聲明處,我們稱之為聲明處變型。這與 Java 中的使用處變型相反。

另外除了 out,Kotlin 又補充了一個變型注釋:in。
它接受一個類型參數(shù)逆變:只可以被消費而不可以 被生產(chǎn)。非變型類的一個很好的例子是 Comparable:

abstract class Comparable<in T> {
    abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

使用處變型:類型投影*

聲明類型參數(shù) T 為 out 很方便,而且可以避免在使用出子類型的麻煩,但有些類 不能 限制它只返回 T ,Array 就是一個例子:

class Array<T>(val size: Int) {
    fun get(index: Int): T { /* ... */ }
    fun set(index: Int, value: T) { /* ... */ }
}

這個類既不能是協(xié)變的也不能是逆變的,這會在一定程度上降低靈活性。考慮下面的函數(shù):

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

該函數(shù)作用是復(fù)制 array ,讓我們來實際應(yīng)用一下:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any) // Error: expects (Array<Any>, Array<Any>)

這里我們又遇到了同樣的問題 Array<T> 中的T 是不可變型的,因此 Array<Int> 和 Array<Any> 互不為對方的子類,導(dǎo)致復(fù)制失敗。為什么呢?應(yīng)為復(fù)制可能會有不合適的操作,比如嘗試寫入,當(dāng)我們嘗試將 Int 寫入 String 類型的 array 時候?qū)?dǎo)致 ClassCastException 異常。

我們想做的就是確保 copy() 不會做類似的不合適的操作,為阻止向from寫入,我們可以這樣:

fun copy(from: Array<out Any>, to: Array<Any>) {
 // ...
}

這就是類型投影:這里的from不是一個簡單的 array, 而是一個投影,我們只能調(diào)用那些返回類型參數(shù) T 的方法,在這里意味著我們只能調(diào)用get()。這是我們處理調(diào)用處變型的方法,類似 Java 中Array<? extends Object>,但更簡單。

當(dāng)然也可以用in做投影:

fun fill(dest: Array<in String>, value: String) {
    // ...
}

Array<in String> 對應(yīng) Java 中的 Array<? super String>,fill()函數(shù)可以接受任何CharSequence 類型或 Object類型的 array 。

參考文章:
kotlin-in-chinese

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容