Kotlin系列——泛型型變

本文章已授權(quán)微信公眾號郭霖(guolin_blog)轉(zhuǎn)載骇钦。

本文章講解的內(nèi)容是泛型型變宰啦,我寫一個擴(kuò)展Boolean示例代碼來應(yīng)用我要講的內(nèi)容允懂,示例代碼如下:

BooleanExtensionDemo

先看下以下例子汽馋,代碼如下:

List<String> strings = new ArrayList<String>();
// Java中禁止這樣的操作
List<Object> objects = strings;

Java中是禁止這樣的操作的偷遗,我們看下Kotlin的寫法醉拓,代碼如下:

val strings: List<String> = arrayListOf()
val anys: List<Any> = strings

Kotlin中是允許這樣的操作的伟姐,這是為什么呢?下面會詳細(xì)解釋亿卤。

List<String>中愤兵,List基礎(chǔ)類型String類型實參排吴,現(xiàn)有兩個List集合秆乳,分別是List<String>List<Any>,它們都具有相同基礎(chǔ)類型,但是類型實參不相同屹堰,并且StringAny存在父子關(guān)系肛冶,型變就是指List<String>List<Any>這兩者存在什么關(guān)系。

形式參數(shù)和實際參數(shù)

函數(shù)中的形參和實參

代碼如下:

fun add(firstNumber: Int, secondNumber: Int): Int =
    firstNumber + secondNumber

firstNumbersecondNumber就是形式參數(shù)扯键,然后去調(diào)用這個函數(shù)睦袖,代碼如下:

val first = 1
val second = 2
add(first, second)

firstsecond就是add函數(shù)實際參數(shù)

泛型中的形參和實參

代碼如下:

class Fruit<T>(var item: T)

T就是類型形參荣刑,然后使用這個馅笙,代碼如下:

val fruit = Fruit<Int>(100)

Int就是Fruit類型實參,因為Kotlin具有類型推導(dǎo)特性厉亏,不必明確指明類型董习,所以其實可以寫成如下代碼:

val fruit = Fruit(100)

在這種情況下,Int依然是Fruit類型實參爱只。

還有以下情況皿淋,請看代碼:

// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {
    // 省略部分代碼
}

這里的EListMutableCollection類型實參,同時是MutableList類型形參虱颗。

結(jié)論

定義在里面就是形式參數(shù)沥匈,定義在外面就是實際參數(shù)。

子類忘渔、超類高帖、子類型、超類型

子類繼承超類畦粮,例如class Apple: Fruit()散址,Apple就是Fruit子類Fruit就是Apple超類宣赔,那什么是子類型超類型呢预麸?它們的規(guī)則比子類超類更加寬松,如果需要A類型的地方儒将,都可以用B類型來代替吏祸,那么B類型就是A類型的子類型,A類型就是B類型的超類型钩蚊,例如StringString?贡翘,如果一個函數(shù)接收的是String?,我們傳入的是String的話砰逻,編譯器是不會報錯的鸣驱,但是如果一個函數(shù)接受的是String,我們傳入的是String?的話蝠咆,編譯器就會提示我們可能會存在空指針的問題踊东,所以String就是String?的子類型北滥,String?就是String的超類型。

子類型化關(guān)系

如果需要A類型的地方闸翅,都可以用B類型來代替再芋,那么B類型就是A類型的子類型,B類型到A類型之間的映射關(guān)系就是子類型化關(guān)系缎脾,舉個例子:List<String>List<Any>子類型祝闻,所以List<String>List<Any>之間存在子類型化關(guān)系占卧,List<String>List<String?>子類型遗菠,所以List<String>List<String?>之間存在子類型化關(guān)系MutableList<String>MutableList<Any>之間就沒有關(guān)系华蜒,這個會在下面解釋辙纬。

協(xié)變

協(xié)變(convariant)就是保留子類型化關(guān)系保證泛型內(nèi)部操作該類型時是只讀的叭喜,在Java中贺拣,帶extends限定(上界)通配符類型使得類型是協(xié)變的。

因為List<out E>協(xié)變捂蕴,StringAny子類型譬涡,StringString?子類型,所以List<String>List<Any>子類型啥辨,List<String>List<String?>子類型涡匀。

out協(xié)變點

以下代碼是標(biāo)準(zhǔn)out協(xié)變點

// T被聲明為out
interface Producer<out T> {

    // T作為只讀屬性的類型
    val value: T

    // T作為函數(shù)返回值的類型
    fun produce(): T

    // T作為只讀屬性的類型List泛型的類型實參
    val list: List<T>

    // T作為函數(shù)返回值的類型List泛型的類型實參
    fun produceList(): List<T>

}

out協(xié)變點基本特征:出現(xiàn)的位置是只讀屬性的類型或者函數(shù)的返回值類型,它作為生產(chǎn)者的角色溉知,請求向外部輸出陨瘩。

源碼分析

源碼中,最為代表性就是List<out E>级乍,代碼如下:

// Collections.kt
// E被聲明為out
public interface List<out E> : Collection<E> {

    override val size: Int
    override fun isEmpty(): Boolean

    // E作為函數(shù)形參類型舌劳,而且還加上了@UnsafeVariance注解,下面會解釋
    override fun contains(element: @UnsafeVariance E): Boolean

    // E作為函數(shù)返回值的類型Iterator泛型的類型實參
    override fun iterator(): Iterator<E>

    // E作為函數(shù)形參的類型Collection泛型的類型實參玫荣,而且還加上了@UnsafeVariance注解甚淡,下面會解釋
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    // E作為函數(shù)返回值的類型
    public operator fun get(index: Int): E

    // E作為函數(shù)形參類型,而且還加上了@UnsafeVariance注解捅厂,下面會解釋
    public fun indexOf(element: @UnsafeVariance E): Int

    // E作為函數(shù)形參類型贯卦,而且還加上了@UnsafeVariance注解,下面會解釋
    public fun lastIndexOf(element: @UnsafeVariance E): Int

    // E作為函數(shù)返回值的類型ListIterator泛型的類型實參
    public fun listIterator(): ListIterator<E>

    // E作為函數(shù)返回值的類型ListIterator泛型的類型實參
    public fun listIterator(index: Int): ListIterator<E>

    // E作為函數(shù)返回值的類型List泛型的類型實參
    public fun subList(fromIndex: Int, toIndex: Int): List<E>

}

逆變

逆變(contravariance)就是反轉(zhuǎn)子類型化關(guān)系恒傻,保證泛型內(nèi)部操作該類型時是只寫的脸侥,在Java中,帶super限定(下界)通配符類型使得類型是逆變的盈厘。

因為Comparable<in T>逆變睁枕,StringAny子類型斋荞,StringString?子類型励两,所以Comparable<Any>Comparable<String>子類型Comparable<String?>Comparable<String>子類型

in逆變點

以下代碼是標(biāo)準(zhǔn)in逆變點

// T被聲明為in
interface Consumer<in T> {

    // T作為函數(shù)形參類型
    fun consume(value: T)

    // T作為函數(shù)形參的類型List泛型的類型實參
    fun consumeList(list: List<T>)

}

in逆變點基本特征:出現(xiàn)的位置是函數(shù)形參類型赏廓,它作為消費者,請求外部輸入嘴高。

源碼分析

源碼中浸策,最為代表性就是Comparable<in T>,代碼如下:

// Comparable.kt
// T被聲明為in
public interface Comparable<in T> {

    // T作為函數(shù)形參類型
    public operator fun compareTo(other: T): Int

}

不型變

不型變就是既不被聲明為out菲语,也不被聲明為in泛型妄辩。

因為MutableList<E>不型變,雖然StringAny子類型山上,StringString?子類型眼耀,但是MutableList<String>MutableList<Any>之間沒有任何關(guān)系MutableList<String>MutableList<String?>之前沒有任何關(guān)系佩憾。

不型變的基本特征:可以出現(xiàn)在任何位置哮伟。

源碼分析

源碼中,最為代表性就是MutableList<E>妄帘,代碼如下:

// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> {

    // E作為函數(shù)形參類型
    override fun add(element: E): Boolean

    // E作為函數(shù)形參類型
    override fun remove(element: E): Boolean

    // E作為函數(shù)形參的類型Collection泛型的類型實參
    override fun addAll(elements: Collection<E>): Boolean

    // E作為函數(shù)形參的類型Collection泛型的類型實參
    public fun addAll(index: Int, elements: Collection<E>): Boolean

    // E作為函數(shù)形參的類型Collection泛型的類型實參
    override fun removeAll(elements: Collection<E>): Boolean

    // E作為函數(shù)形參的類型Collection泛型的類型實參
    override fun retainAll(elements: Collection<E>): Boolean
    override fun clear(): Unit

    // E作為函數(shù)形參類型
    public operator fun set(index: Int, element: E): E

    // E作為函數(shù)形參類型
    public fun add(index: Int, element: E): Unit

    // E作為函數(shù)返回值的類型
    public fun removeAt(index: Int): E

    // E作為函數(shù)返回值的類型MutableListIterator泛型的類型實參
    override fun listIterator(): MutableListIterator<E>

    // E作為函數(shù)返回值的類型MutableListIterator泛型的類型實參
    override fun listIterator(index: Int): MutableListIterator<E>

    // E作為函數(shù)返回值的類型MutableList泛型的類型實參
    override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>

}

@UnsafeVariance

在上面說的List<out E>源碼中楞黄,我們發(fā)現(xiàn)雖然List<out E>協(xié)變的,但是有時出現(xiàn)的位置是逆變的位置抡驼,這是為什么呢鬼廓?其實是可以出現(xiàn)在任何位置上,但是要保證以下兩點定義:協(xié)變保證泛型內(nèi)部操作類型時是只讀的婶恼,逆變保證泛型內(nèi)部操作類型時是只寫的桑阶,大體上要遵循上面說的那幾個out協(xié)變點和in逆變點

我們可以通過加上@UnsafeVariance注解告訴編譯器這個地方是合法勾邦、安全蚣录,讓其通過編譯,如果不加的話眷篇,編譯器會認(rèn)為你這里是不合法萎河,編譯不通過。

例如上面說的List<out E>源碼中蕉饼,有一個contains函數(shù)虐杯,這個函數(shù)的作用是檢查此元素是否包含在此集合中,它的實現(xiàn)方法沒有出現(xiàn)寫操作昧港,所以這里就可以加上@UnsafeVariance注解擎椰,讓其通過編譯器。

使用處型變和聲明處型變

Java是使用使用處型變创肥,有如下接口

public interface IGeneric<T> {
    // 省略部分代碼
}

Java禁止這樣的操作的:

private void setData(IGeneric<String> item) {
    // Java禁止這樣的操作
    IGeneric<Object> newItem = item;
}

我們應(yīng)該寫成如下這樣:

private void setData(IGeneric<String> item) {
    IGeneric<? extends Object> newItem = item;
}

我們必須把newItem聲明為IGeneric<? extends Object>达舒,類型變得更復(fù)雜了值朋,復(fù)雜的類型并沒有給我們帶來任何價值,這種就叫做使用處型變巩搏,我們看下Kotlin的寫法吧昨登,有如下接口

// T被聲明為out
interface IGeneric<out T> {
    // 省略部分代碼
}

有如下方法

private fun setData(item: IGeneric<String>) {

    // 泛型IGeneric的類型實參是Any
    val newItem: IGeneric<Any> = item

}

這種就做聲明處型變,我們只需要在用out修飾符修飾T即可贯底,語義簡單了很多丰辣,當(dāng)然Kotlin也可以使用使用處型變的,我們不再用out修飾符修飾T禽捆,代碼如下:

interface IGeneric<T> {
    // 省略部分代碼
}

然后我們在聲明類型的時候加上out修飾符笙什,代碼如下:

private fun setData(item: IGeneric<String>) {

    // 泛型IGeneric的類型實參Any被聲明為out
    val newItem: IGeneric<out Any> = item

}

星投影

定義

有時候,我們對類型參數(shù)一無所知睦擂,但是仍然希望以安全的方式使用它得湘,我們可以使用星投影杖玲,這個泛型類型的每個具體實例化是這個投影的子類型顿仇。

語法

  • 對于Function<out T : String>T泛型Function的一個具有上界String協(xié)變類型參數(shù)摆马,Function<>等價于Function<out String>臼闻,這意味著當(dāng)T未知時,我們可以安全地從Function<>讀取String**的值囤采。
  • 對于Function<in T>述呐,T泛型Function的一個逆變類型參數(shù)Function<>等價于Function<in Nothing>蕉毯,這意味著當(dāng)T未知時乓搬,我們不能安全地寫入Function<>**。
  • 對于Function<T : String>代虾,T泛型Function的一個不型變類型參數(shù)进肯,Function<>讀取值時等價于Function<out String>寫入值時等價于Function<in Nothing>*棉磨。

如果一個泛型類型具有多個類型參數(shù)江掩,那么它們每個類型參數(shù)都可以單獨投影,例如:如果類型被聲明為Function<in T, out U>乘瓤,那么它的星投影就如下:

  • Function<, String>表示Function<in Nothing, String>*
  • Function<String, >表示Function<String, out Any?>*
  • Function<, >表示Function<in Nothing, out Any?>

要注意的是星投影非常像Java原始類型环形,但是是安全的。

這里解釋一下Nothing衙傀,Nothing是所有類型的子類型抬吟,源碼如下:

public class Nothing private constructor()

應(yīng)用

我們可以擴(kuò)展Boolean,讓其更具有函數(shù)式編程的味道统抬,讓鏈?zhǔn)秸{(diào)用更加順滑火本,代碼如下:

package com.tanjiajun.booleanextensiondemo

/**
 * Created by TanJiaJun on 2020-01-28.
 */
sealed class BooleanExt<out T>

class TransferData<T>(val data: T) : BooleanExt<T>()
object Otherwise : BooleanExt<Nothing>()

inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> =
    when {
        this -> TransferData(block.invoke())
        else -> Otherwise
    }

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T =
    when (this) {
        is Otherwise -> block()
        is TransferData -> data
    }

調(diào)用地方任洞,代碼如下:

package com.tanjiajun.booleanextensiondemo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

/**
 * Created by TanJiaJun on 2020-01-28.
 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 第一個例子
        val name = "譚嘉俊"
        (name == "譚嘉俊")
            .yes { Log.i("TanJiaJun", name) }
            .otherwise { Log.i("TanJiaJun", "蘋果") }

        // 第二個例子
        val strings = mutableListOf(2, 4, 6, 8, 10)
        (strings
            .filter { it % 2 == 0 }
            .count() == strings.size)
            .yes { Log.i("TanJiaJun", "是偶數(shù)集合") }
            .otherwise { Log.i("TanJiaJun", "不是偶數(shù)集合") }
    }

}

我們可以看到密封類BooleanExt,它是個泛型发侵,T是一個協(xié)變類型參數(shù)交掏,為什么要用到協(xié)變呢?我們可以觀察到T都出現(xiàn)在out協(xié)變點刃鳄,所以T可以被聲明為out盅弛。

我們還看到對象Otherwise繼承密封類BooleanExt,我使用了Nothing叔锐,為什么要使用Nothing呢挪鹏?因為在Boolean擴(kuò)展函數(shù)yes中返回的是BooleanExt<T>,如果要返回Otherwise愉烙,我們就只能使用Nothing讨盒,因為Nothing是所有類型的子類型,上面也提及過步责,所以這樣就符合協(xié)變定義了返顺。

題外話

PECS原則

PECS原則是指Producer-Extends, Consumer-Super,它是Effective Java提出來的蔓肯,如果泛型類型實參生產(chǎn)者遂鹊,那么就應(yīng)該用extends;如果泛型類型實參消費者蔗包,那么就應(yīng)該用super秉扑。

密封類

在我的示例代碼中,我用到了sealed這個修飾符调限,它可以聲明一個密封類舟陆,我這里大概說下密封類

  • 密封類用來表示受限的類繼承結(jié)構(gòu),意思就是當(dāng)一個值為有限幾種的類型耻矮,而不能有任何其他類型秦躯,其實他們在某種意義上,有點像枚舉類擴(kuò)展淘钟,不過枚舉類型的值集合是受限的宦赠,每個枚舉常量只存在一個實例,而密封類一個子類可以有包含狀態(tài)的多個實例米母。

  • 密封類可以有子類勾扭,但是所有子類必須在與密封類自身相同文件中聲明。

  • 密封類自身是抽象的铁瞒,它不能直接實例化妙色,但是可以有抽象成員

  • 密封類不允許有非private構(gòu)造函數(shù)慧耍,它的構(gòu)造函數(shù)就是private的身辨。

  • 擴(kuò)展密封類子類的類可以放在任何位置丐谋,而不需要放在同一個文件中

  • 使用密封類還有個好處在于使用when表達(dá)式的時候煌珊,當(dāng)我們用when作為表達(dá)式的時候号俐,也就像上面示例代碼中的otherwise,我是使用了它的結(jié)果定庵,而不是作為語句吏饿,如果能夠驗證語句覆蓋了所有情況的時候,我們就不需要再為語句添加一個else子句了蔬浙。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:譚嘉俊

我的簡書:譚嘉俊

我的CSDN:譚嘉俊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猪落,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子畴博,更是在濱河造成了極大的恐慌笨忌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俱病,死亡現(xiàn)場離奇詭異官疲,居然都是意外死亡,警方通過查閱死者的電腦和手機庶艾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門袁余,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咱揍,你說我怎么就攤上這事∨锒” “怎么了煤裙?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長噪漾。 經(jīng)常有香客問我硼砰,道長,這世上最難降的妖魔是什么欣硼? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任题翰,我火速辦了婚禮,結(jié)果婚禮上诈胜,老公的妹妹穿的比我還像新娘豹障。我一直安慰自己,他們只是感情好焦匈,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布血公。 她就那樣靜靜地躺著,像睡著了一般缓熟。 火紅的嫁衣襯著肌膚如雪累魔。 梳的紋絲不亂的頭發(fā)上摔笤,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機與錄音垦写,去河邊找鬼吕世。 笑死,一個胖子當(dāng)著我的面吹牛梯投,可吹牛的內(nèi)容都是我干的寞冯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼晚伙,長吁一口氣:“原來是場噩夢啊……” “哼吮龄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咆疗,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤漓帚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后尝抖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡昧辽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年搅荞,在試婚紗的時候發(fā)現(xiàn)自己被綠了框咙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡茉贡,死狀恐怖者铜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情作烟,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布科汗,位于F島的核電站绷雏,受9級特大地震影響怖亭,放射性物質(zhì)發(fā)生泄漏坤检。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一倾芝、第九天 我趴在偏房一處隱蔽的房頂上張望箭跳。 院中可真熱鬧晨另,春花似錦谱姓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茂契。三九已至,卻和暖如春掉冶,著一層夾襖步出監(jiān)牢的瞬間儡蔓,已是汗流浹背郭蕉。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旁振,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓吉嚣,卻偏偏與公主長得像蹬铺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子秋泄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容