Kotlin學習筆記 - 泛型

1. 基本用法

class Box<T> {

    private var element: T? = null

    fun add(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

2. 型變

型變包括 協變、逆變、不變 三種:

  • 協變:泛型類型與實參的繼承關系相同
  • 逆變:泛型類型與實參的繼承關系相反
  • 不變:泛型類型與實參類型相同

Java的型變

首先明確一點,Java不直接支持型變。通俗地講,雖然Integer是Number的子類,但是在Java中,List<Integer>和List<Number>是沒有關系的(List<Integer>不是List<Number>的子類)所以List<Integer>不能直接賦值給List<Number>。

如果Java支持型變,則會出現以下情況:

// error:以下代碼實際上會編譯報錯
List<Number> numList = new ArrayList<Integer>();
// 假設以上代碼能夠編譯通過,以下代碼就會在運行時引發異常
// numList實際上操作的集合元素必須是Integer類型
numList.add("haha"); // ClassCastException

Java采用通配符的方式來處理型變的需要(泛型通配符)

public interface Collection<E> extends Iterable<E> {
    // Java泛型通配符上限(協變),<? extends E> 可用于接收 E 或 E 的子類型
    boolean addAll(Collection<? extends E> c);
    
    // Java泛型通配符下限(逆變),<? super E> 可用于接收 E 或 E 的父類型
    boolean removeIf(Predicate<? super E> filter);
}

以上是泛型通配符在Collection接口源碼中的使用

  • 通配符上限
public static void test(List<? extends Number> list) {
    /*
     * 對于“通配符上限”語法而言,從該集合中取出元素是安全的。
     * 集合中的元素是Number或其子類型,但是不能往該集合中存入新的元素,
     * 因為無法預測該集合元素的實際類型是Integer還是Double,又或者是Number的其他子類型
     */
    for (Number num : list) {
        System.out.println(num);
    }
    // list.add() // 不能添加元素,因為不能確定元素的類型
}
  • 通配符下限
public static void test(List<? super Integer> list) {
    /*
     * 對于"通配符下限"語法而言,將對象傳給泛型對象是安全的。
     * 該集合中的元素是Integer或其父類型,但是從該集合中取出元素是不安全,
     * 因為無法預測該集合元素的實際類型是Number還是Object
     */
    list.add(666);
    // 以下代碼編譯不通過,因為在取出元素時無法確認元素的類型,可能是Number類型,也可能是Object類型
    // Number number = list.get(0);
}

泛型的規律

  1. 通配符上限(泛型協變)意味著從中取出<font color=red>(out)</font>對象是安全的,但傳入對象是不安全的
  2. 通配符下限(泛型逆變)意味著向其中傳入<font color=red>(in)</font>對象是安全的,但取出對象是不安全的

Kotlin的型變

Kotlin處理泛型型變的規則就是根據泛型的規律而設計:

  1. 如果泛型只需要出現在方法的返回值申明中(不出現在形參的聲明中),那么該方法就只是取出泛型對象,因此該方法就支持泛型協變(相當于通配符上限);如果一個類的所有方法都支持泛型協變,那么該類的泛型參數可使用out修飾。
  2. 如果泛型只需要出現在方法的形參聲明中(不出現在返回值聲明中),那么該方法就只是傳入泛型對象,因此該方法就支持泛型逆變(相當于通配符下限);如果一個類的所有方法都支持泛型逆變,那么該類的泛型參數可使用in修飾。

聲明處型變

  • 如果一個類的所有方法都支持泛型協變,那么該類的泛型參數可使用out修飾
class Box<out T> {
    private var element: T? = null
    
    fun get(): T? = element
}

fun main(args: Array<String>) {
    // 由于泛型使用out修飾,所以 Box<Int> 對象可以直接賦值給 Box<Number>
    var box: Box<Number> = Box<Int>()
}
  • 如果一個類的所有方法都支持泛型逆變,那么該類的泛型參數可使用in修飾
class Box<in T> {
    private var element: T? = null

    fun put(e: T) {
        this.element = element
    }
}

fun main(args: Array<String>) {
    // 由于泛型使用in修飾,所以 Box<Any> 對象可以直接賦值給 Box<Number>
    var box: Box<Number> = Box<Any>()
}

聲明處型變的限制:</br>如果一個類中有的方法使用泛型聲明返回值類型,有的方法使用泛型聲明形參類型,那么該類就不能使用聲明處型變。

使用處型變

class Box<T> {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

fun main(args: Array<String>) {
    // 使用 out 修飾泛型
    var outBox: Box<out Number> = Box<Int>()
    // 不能調用put方法存入數據(編譯報錯)
    // outBox.put(18)
    val num: Number? = outBox.get()

    // 使用 in 修飾泛型
    var inBox: Box<in Int> = Box<Number>()
    inBox.put(18)
    // 不能確定返回值的類型,只能使用Any類型接收
    val e: Any? = inBox.get()
}

@UnsafeVariance注解

對于協變的類型,通常是不允許將泛型類型作為方法參數的類型,但是在某些情況下,我們需要在協變的情況下將泛型作為方法的參數類型,那么我們可以使用 @UnsafeVariance 注解來修飾泛型。

class Box<out T> {
    private var element: T? = null

    // 如果開發者自己可以保證類型安全,那么可以使用@UnsafeVariance注解讓編譯器不報錯
    fun put(element: @UnsafeVariance T) {
        this.element = element
    }

    fun get(): T? = element
}

星投影

在Java中,當我們不確定泛型的具體類型是,可以使用 ? 來代替具體的泛型,比如

List<?> list = new ArrayList<String>();

在Kotlin也有類似的語法,可以使用 * 來指代相應的泛型映射

// 星投影
val list: List<*> = ArrayList<String>()

以下是官方對于星投影語法的解釋:

  • <p>對于 Foo <out T>,其中 T 是一個具有上界 TUpper 的協變類型參數,Foo <> 等價于 Foo <out TUpper>。 這意味著當 T 未知時,你可以安全地從 Foo <> 讀取 TUpper 的值。</p>
  • <p>對于 Foo <in T>,其中 T 是一個逆變類型參數,Foo <> 等價于 Foo <in Nothing>。 這意味著當 T 未知時,沒有什么可以以安全的方式寫入 Foo <>。</p>
  • <p>對于 Foo <T>,其中 T 是一個具有上界 TUpper 的不型變類型參數,Foo<*> 對于讀取值時等價于 Foo<out TUpper> 而對于寫值時等價于 Foo<in Nothing>。</p>
簡而言之,星投影就是:
  • 當 * 接收可協變的泛型參數 ( out T ) 時,* 映射的類型為 Any?
class Box<out T> {
    private var element: T? = null

    fun get(): T? = element
}

fun main(args: Array<String>) {
    val intBox: Box<Int> = Box()

    val numBox: Box<Number> = intBox
    val num: Number? = numBox.get()

    // 星投影,這里的 <*> 相當于 <out Int>,元素類型映射為 Any?
    val box: Box<*> = intBox
    val element: Any? = box.get()
}
  • 當 * 接收可逆變的泛型參數 ( in T ) 時,* 映射的類型為 Nothing
class Box<in T> {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }
}

fun main(args: Array<String>) {
    // 星投影,這里的 <*> 相當于 <in Number>,元素類型映射為 Nothing
    val box: Box<*> = Box<Number>()
    box.put(/*element:Nothing*/) // 沒有值可以傳入
}
  • 當 * 接收不變的泛型參數 ( T ) 時,* 對于讀取值類型映射為 Any?,而寫值時類型映射為 Nothing
lass Box<T> {
    private var element: T? = null

    fun put(element: T) {
        this.element = element
    }

    fun get(): T? = element
}

fun main(args: Array<String>) {
    // 星投影
    val box: Box<*> = Box<Number>()
    // 這里的 <*> 在賦值時相當于 <in Int>,元素類型映射為 Nothing
    box.put(/*element:Nothing*/) // 沒有值可以傳入
    // 這里的 <*> 在取值時相當于 <out Int>,元素類型映射為 Any?
    val element: Any? = box.get()
}

3. 泛型函數

泛型函數就是在函數聲明時定義一個或多個泛型,泛型的聲明必須在 fun 與函數名之間

fun <T> test(a: T) {
    println(a)
}

fun main(args: Array<String>) {
    test<Int>(1) // <Int>可省略,類型通過參數自動推斷
    test(2)
}

泛型函數也可以用于擴展函數

fun <T> T.test(): String {
    return "test(): ${this.toString()}"
}

fun main(args: Array<String>) {
    val num = 666
    // 顯示指定泛型為 Int 類型,<Int>可省略
    println(num.test<Int>())

    // 不顯示指定泛型的類型,編譯器自動推斷出泛型為 String 類型
    val str = "haha"
    println(str.test())
}

4. 具體化類型參數

Kotlin允許在內聯函數中使用 reified 修飾泛型參數,這樣就可以將該泛型參數變成一個具體化的類型參數。具體化類型參數后就可以在函數中將泛型當做一個普通類型來使用,比如可以使用 is、as 運算符。

比如,我們需要在list中找到指定類型的元素,原先的寫法如下

fun <T> findData(list: List<*>, clazz: Class<T>): T? {
    for (e in list) {
        if (clazz.isInstance(e)) {
            @Suppress("UNCHECKED_CAST")
            return e as T
        }
    }
    return null
}

fun main(args: Array<String>) {
    val list = listOf(1, 6.6f, "haha")
    println(findData(list, String::class.java)) // haha
    println(findData(list, Float::class.javaObjectType)) // 6.6
}

使用具體化類型參數之后,代碼如下:

// 很明顯,代碼變得更簡潔了
inline fun <reified T> findData(list: List<*>): T? {
    for (e in list) {
        if (e is T) {
            return e
        }
    }
    return null
}

fun main(args: Array<String>) {
    val list = listOf(1, 6.6f, "haha")
    println(findData<String>(list))
    println(findData<Float>(list))
}

使用reified修飾的泛型參數,還可以對其使用反射

inline fun <reified T> test() {
    println(T::class.java)
}

fun main(args: Array<String>) {
    test<String>() // class java.lang.String
    test<Double>() // class java.lang.Double
}

5. 泛型邊界(上界)

java中可以使用 extends 指定泛型的上界

class Box<T extends Number> {
    // ...
}

Koltin的實現:

class Box<T : Number> {
    // ...
}

如果需要指定多個邊界,需要使用where子句(只能有一個父類上界,可以有多個接口上界)

class Box<T> where T : Number, T : Comparable<T> {
    // ...
}

PS:在Kotlin中,如果不指定邊界,則默認邊界是 Any#

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370