Kotlin的協(xié)變和逆變

前言

?最近在圖書館翻書,偶然看到郭神最新版的第一行代碼,感慨頗深,想當(dāng)初基本就是跟著這本書入門的,一晃眼好幾年過去了。就想著說翻翻看,不翻還好,一翻又跪倒在郭神腳下了。大牛之所以是大牛,他就是能用最淺顯最樸素的語言,把難懂的知識講解給你聽,沒有那么多花里胡哨和炫技,而你也一聽就能懂。

?翻到了Koltin的協(xié)變和逆變的時候,想起當(dāng)初在學(xué)習(xí)這里看官方文檔還是一些文章的時候,確實云里霧里,似懂非懂。而且郭神也很明確提到,他一開始學(xué)這方面知識的時候,查到的資料大都晦澀難懂,導(dǎo)致對這兩個知識點產(chǎn)生了些畏懼。好家伙,這說的不就是我嗎,但最終郭神還是能啃下來,而我就不行了。所以,借著這個機會想著把Kotlin簡單的梳理一遍。

?就先從協(xié)變和逆變開始入手了。其實你可能感覺,我們平時在用Kotlin的時候,似乎基本都用不上這兩個東西啊。沒錯,確實我自己也基本沒用到過...但是你會發(fā)現(xiàn),Kotlin本身內(nèi)置的API使用了很多協(xié)變和逆變的特性(后面說),所以如果想對Kotlin有更深刻的了解,我們還是得學(xué)的。

先導(dǎo)知識

?一個泛型類或者泛型接口中的方法,它的參數(shù)列表是接受數(shù)據(jù)的地方,我們稱之為in;而它的返回值是輸出數(shù)據(jù)的地方,我們稱之為out。這個很好理解對吧,但是到后面你可能就會亂了,反正我們先記住,這是基礎(chǔ),很重要。還有就是其實協(xié)變和逆變就是用來輔助泛型的,所以我們都是和泛型類或者泛型接口打交道。

interface MyClass<T> {
    fun method(params: T): T
}

這邊方法內(nèi)的入?yún)arams(第一個T)就是in所在位置,第二個T則是out所在位置。

我們定義三個類:

open class People(val name: String, val age: Int)
class Teacher(name: String, age: Int): People(name, age)
class Student(name: String, age: Int): People(name, age)

父類People,子類Teacher和Student,兩個子類平級。

協(xié)變

?那么,這里引出一個問題:如果一個方法接收People類型的參數(shù),那么我們?nèi)我鈧魅隩eacher和Student的實例都沒問題吧?因為兩個都是People的子類,老師和學(xué)生都是人。

?升級一下問題:如果這個方法接收的是List<People>類型的參數(shù),那么我們還能傳入List<Teacher>或List<Student>類型的實例嗎?很顯然,你一傳,編譯器就給你一巴掌,跟你說List<Teacher>并不是List<People>的子類,存在類型轉(zhuǎn)換的安全隱患。事實也是,你List<A>怎么會是List<B>的子類呢,它兩都是List。

?舉個實際的例子,我們有個Data泛型類,內(nèi)部封裝了一個泛型value字段:

class Data<T> {
    private var value: T? = null
    
    fun set(value: T) {
        this.value = value
    }

    fun get(): T? {
        return value
    }
}

?接著我們來嘗試一下上面的情況,第一步先創(chuàng)建一個持有Student的Data類實例,第二步再創(chuàng)建一個接收Data<People>類型參數(shù)的方法,我們在方法內(nèi)重新創(chuàng)建一個Teacher的實例,設(shè)置給Data。

fun main() {
        val student = Student("Tom", 18)
        //創(chuàng)建一個持有Student的Data類
        val studentData = Data<Student>()
        studentData.set(student)

        //將持有Student的Data類實例傳給接收Data<People>類型參數(shù)的方法
        handlePeopleData(studentData)

        //最后我們再把數(shù)據(jù)取出來看看
        val curStudentData = studentData.get()
    }

fun handlePeopleData(data: Data<People>) {
        //這里我們創(chuàng)建一個Teacher的實例,把data持有的數(shù)據(jù)替換掉
        val teacherData = Teacher("Jason", 38)
        data.set(teacherData)
    }

?很顯然,第一步肯定沒問題,第二步的話因為Data要求的泛型是People類,而我們創(chuàng)建的Teacher是繼承People類的,所以也沒問題。但是如果我們要將第一步創(chuàng)建的studentData傳入到第二步創(chuàng)建的方法里,編譯器就會報錯:

Type mismatch.
Required:Data<People>
Found:Data<Student>

?就是我們料想的那樣。那如果說,我們假設(shè)這里編譯能通過,正常把studentData傳到方法里面來了。那方法跑完,我們再回頭看main()方法的最后一行,我們再通過get方法把Data持有的實力取出來,發(fā)現(xiàn)此時Data<Student>中持有的是Teacher的實例,那么就必然會產(chǎn)生類型轉(zhuǎn)換異常了。

?所以Java是不允許使用這種方式來傳遞參數(shù)的??墒怯袝r候我實際就是會遇上這種情況呢,我就是希望不做多余的轉(zhuǎn)換類型的步驟,能達到這樣的傳遞效果呢。那么,協(xié)變就上場了。

?我們定義一個泛型類MyClass<T>,類A和類B,其中A是B的子類型,而同時MyClass<A>又是MyClass<B>的子類型,那我們就稱MyClass在T這個泛型上是協(xié)變的。有點繞,可以反過來理解,我們可以使用協(xié)變來達到讓MyClass<A>成為MyClass<B>的子類型的目的。

?那么具體是怎么做的呢?我們剛剛也分析了,出現(xiàn)類型轉(zhuǎn)換異常的痛點在于我們在方法內(nèi)重新設(shè)置了不同類型的實例。那如果讓Data在泛型T上只讀呢,是不是就沒有這個安全隱患了?沒錯,我們修改一下Data類的代碼:

class Data<out T>(val value: T?) {

    fun get(): T? {
        return value
    }
}

?我們直接通過構(gòu)造函數(shù)來傳遞泛型T類型的數(shù)據(jù),使其無法修改,同時在泛型T的聲明前面加上out關(guān)鍵字。還記得out指代的是什么位置嗎,我們前面說過了,out就是指方法返回值的位置。也就是說,當(dāng)我們加上out來限定當(dāng)前泛型T的時候,我們只能在out指代的位置上獲取到當(dāng)前泛型實例(而無法在in指代的位置傳入泛型實例,為了避免混亂,可以先不管in)。

?你可能會說,構(gòu)造函數(shù)里不就傳入了泛型T嗎?那肯定得做初始化的操作,你一開始創(chuàng)建實例不給它東西,它持有空氣呢,并且val關(guān)鍵字也限定了是只讀的。那么我們再修改一下之前的代碼:

fun main() {
        val student = Student("Tom", 18)
        //創(chuàng)建一個持有Student的Data類
        val studentData = Data<Student>(student)

        //將持有Student的Data類實例傳給接收Data<People>類型參數(shù)的方法
        handlePeopleData(studentData)

        //最后我們再把數(shù)據(jù)取出來看看
        val curStudentData = studentData.get()
}

private fun handlePeopleData(data: Data<People>) {
        //只做讀取邏輯
        val get = data.get()
 }

?這個時候我們驚奇的發(fā)現(xiàn),編譯器不報錯了。那也就是說我們已經(jīng)讓Data<Student>成功的成為 Data<People>的子類型,那自然就能傳給handlePeopleData()方法了,是不是很神奇。而方法內(nèi)雖然泛型聲明的是People類型,實際取出來的是一個Student的實例,但由于People是Student的父類,所以向上轉(zhuǎn)型是完全沒問題的。這就是協(xié)變給我們帶來的便利所在。

?當(dāng)然了,我們不能有了新歡就忘了舊愛,其實Java也有給我們提供了解決方案,那就是上界通配符<? extends T>。

List<? extends People> peoples = new ArrayList<>();
List<Student> students = new ArrayList<>();
peoples = students;

?很明顯,我們能看到持有泛型為Student類型的List,可以賦值給泛型上界為People類型的List實例。這也很好理解,我們通過上界通配符<? extends People>規(guī)定了只要是持有泛型為People子類類型的List,都算是List<? extends People>的子類型,和Kotlin的out異曲同工。

?那么我們剛剛提到的Kotlin使用到協(xié)變的內(nèi)置API其實一個典型的例子就是List。

public interface List<out E> : Collection<E> {
    // Query Operations

    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    // Bulk Operations
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    // Positional Access Operations
    /**
     * Returns the element at the specified index in the list.
     */
    public operator fun get(index: Int): E
}

?以上是我截取的部分Kotlin的List源碼,可以看到官方已經(jīng)給我們提供好協(xié)變能力了,所以我們在用的時候并不需要像Java一樣來自己設(shè)置通配符,直接用就行。

?但是這里有一個奇怪的點大家可能會問,你剛剛不是還說通過out關(guān)鍵字限制之后,泛型只能出現(xiàn)在返回值的out位置嗎,為什么contains()方法里(也就是in位置)還是出現(xiàn)了泛型呢?

?這么寫本身確實是不合法的,因為在in位置上出現(xiàn)了泛型E就意味著可能會出現(xiàn)類型轉(zhuǎn)換的安全隱患,但是contains()方法的目標(biāo)非常明確,只是為了判斷當(dāng)前集合中是否包含參數(shù)中傳入的這個元素,并不會涉及到修改當(dāng)前集合的內(nèi)容,所以操作是安全的。為此官方給我們開了個綠色通道,我們可以通過@UnsafeVariance注解來允許泛型出現(xiàn)在in位置上,但使用的前提是你自己要清楚自己在干什么。

?協(xié)變差不多就是這樣了,接下來看看逆變。

逆變

?其實我們聽名字,就有點能聽出來意思了,剛剛協(xié)變是定義一個泛型類MyClass<T>,類A和類B,其中A是B的子類型,而同時MyClass<A>又是MyClass<B>的子類型。那么逆變就是反過來,A是B的子類型,而MyClass<B>卻是MyClass<A>的子類型。開始有點暈了吧?放心,我也暈,不暈才怪呢...

?我們先來定義一個接口,用來執(zhí)行一些轉(zhuǎn)換的操作:

interface Transformer<T> {
    fun transform(t: T): String
}

很簡單,聲明一個接收泛型T類型作為參數(shù)并返回String的方法。接下來我們來實現(xiàn)它:

fun main(args: Array<String>) {
    val transformer = object : Transformer<People> {
        override fun transform(t: People): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(transformer)
}

fun handleTransformer(transformer: Transformer<Student>) {
    val student = Student("tom", 18)
    val result = transformer.transform(student)
}

?這里我們第一步實現(xiàn)了一個泛型為People的匿名類,通過transform()方法把People對象轉(zhuǎn)化為String的返回結(jié)果。第二步再新建了一個handleTransformer()方法,它接收一個泛型為Student的Transformer對象,我們用它來將Student對象轉(zhuǎn)換為字符串。這兩個步驟各自都沒問題,但是當(dāng)我們把泛型為People的匿名類實例傳到handleTransformer()當(dāng)中的時候,就會報錯,而原因還是類型問題。

?這段代碼從安全的角度來分析是沒有任何問題的,因為Student是People的子類,使用Transformer<People>的匿名類來實現(xiàn)(將實例傳到handleTransformer()方法里來)將Student轉(zhuǎn)換成一個字符也是絕對安全的,并不會存在類型轉(zhuǎn)換的安全隱患。

?這個時候,逆變就排上用場了,它就是專門來處理這種情況的。我們修改一下Transformer接口的定義:

interface Transformer<in T> {
    fun transform(t: T): String
}

?很簡單,我們在泛型T的聲明前面加上in關(guān)鍵字來修飾就行了。這樣也就意味著泛型T就只能出現(xiàn)在in位置上,而不能出現(xiàn)在out位置上了,同時表明Transformer類在泛型T上是逆變的。

?這時候你會驚奇的發(fā)現(xiàn),編譯確實通過了,因為此時Transformer<People>已經(jīng)成為了Transformer<Student>的子類型了。這里你也可以直觀的看出來,為什么它叫“逆”變了。那按照這樣的話,聲明完逆變之后,泛型T是不能出現(xiàn)在out位置的,我們來試試:

interface Transformer<in T> {
    fun transform(t: T): String
    fun reverse(name: String, age: Int): @UnsafeVariance T
}

?在接口中新增一個reverse方法,用它來創(chuàng)建泛型T對象。這個時候編譯器就會報錯:Type parameter T is declared as 'in' but occurs in 'out' position in type T,就是說泛型T聲明為“in”(逆變),卻出現(xiàn)在了“out”(協(xié)變)位置上,所以我們先加上@UnsafeVariance注解。

?接著一樣來實現(xiàn)它,并傳到handleTransformer()里:

val transformer = object : Transformer<People> {
        
        override fun transform(t: People): String {
            return "${t.name} ${t.age}"
        }

        override fun reverse(name: String, age: Int): People {
            return Teacher(name, age)
        }
}
handleTransformer(transformer)

?我們在reverse()方法中直接構(gòu)建一個Teacher對象出來,并返回。然后修改一下handleTransformer()方法:

fun handleTransformer(transformer: Transformer<Student>) {
//    val student = Student("tom", 18)
//    val result = transformer.transform(student)

    //for test
    val student = transformer.reverse("tom", 18)
}

?跑一下,這時候我們會“如愿以償”的發(fā)現(xiàn)報錯了(狗頭):

Exception in thread "main" java.lang.ClassCastException: class com.example.jetpack.Teacher cannot be cast to class com.example.jetpack.Student
at com.example.jetpack.CovariantTestKt.handleTransformer(CovariantTest.kt:38)
at com.example.jetpack.CovariantTestKt.main(CovariantTest.kt:29)

?也就說,我們在handleTransformer()方法內(nèi)是期望reverse()出一個Student對象出來,而實際我們得到的卻是Teacher對象,那當(dāng)然就會造成類型轉(zhuǎn)換異常了,因為Teacher并不是Student的子類。

?這也就證明了我們一開始的假設(shè),Kotlin在提供型變(協(xié)變、逆變和不變)的時候,就已經(jīng)把各種潛在的類型轉(zhuǎn)換安全隱患都考慮到了,我們只要嚴(yán)格遵循語法規(guī)則,就不會出現(xiàn)這種類型轉(zhuǎn)換的異常。雖然@UnsafeVariance注解可以打破這一語法規(guī)則,但同時也會帶來對應(yīng)的風(fēng)險,所以我們在使用時要時刻清楚自己是在干什么。

?同樣,Java通過下界通配符<? super T>來提供類似Kotlin逆變的能力。我們先來看一個有趣的情況:

List<? extends People> peoples = new ArrayList<>();
peoples.add(new Student("tom", 18));

?我們通過上界通配符限定了List的泛型,然后我們往List中加入一個新new的Student對象(Student繼承自People),按最直接的思路來說,<? extends People>不就是表明我們要加到List的對象只要繼承People就行嘛。但實際你會發(fā)現(xiàn),編譯器報錯了:

Required type:  capture of ? extends People
Provided: Student

?當(dāng)然了,給大家挖了個坑,這坑很容易讓我們有思維定勢,上述的代碼來說,我們只要定義List持有的泛型是People類型(父類),也就是List(People),那new一個Student對象丟進去就是完全沒問題的。再回到現(xiàn)在的問題,其實剛剛我們也提到了,我們通過上界通配符<? extends People>實現(xiàn)的是讓List<Student>成為List<? extends People>的子類型,是整個List類層次上的限定。而如果直接往List添加Student對象的話,編譯器只能知道類型是People的子類,并不能確定具體類型是什么,因此也無法驗證類型的安全性。

?那如果我們就是想用這種方式處理怎么辦?下界通配符就登場了:

List<? super People> list = new ArrayList<>();
list.add(new Student("tom", 18));

?很神奇,編譯通過了。其實這里我們通過<? super People>限定了泛型為People及其父類,所以list可以接受所有People的子類添加到其中。當(dāng)然還有和Kotlin類似情形的用法:

public static void main(String[] args) {
        People people = new People("Jack", 38);
        SimpleData<People> simpleData2 = new SimpleData<>(people);
        handleData2(simpleData2);
 }

public static void handleData2(SimpleData<? super Student> data) {
       //do something
}

?你可以嘗試一下,這個時候傳一個SimpleData<Student>實例進來,是編譯不了的。好了,扯得有點遠了...

?最后我們再來看下逆變在Kotlin內(nèi)置API中的應(yīng)用,比較典型的例子就是Comparable:

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

?想象一下,我們使用Comparable<People>來實現(xiàn)兩個People年紀(jì)的比較,那么理所當(dāng)然,用這個邏輯去比較兩個Student的年紀(jì)肯定也是沒問題的(因為People是Student父類),所以讓Comparable<People>成為Comparable<Student>的子類就合情合理了。

總結(jié)

?總結(jié)一下,其實不管是協(xié)變還是逆變,都是在類聲明的時候限定泛型的,也就說out限定泛型的時候,是協(xié)變,泛型是只能出現(xiàn)在out位置上,(而不是out關(guān)鍵字要去修飾返回值的位置,這點要理清楚,因為我剛開始接觸的時候就會以為out關(guān)鍵字要修飾返回值);同理當(dāng)in限制泛型的時候,是逆變,泛型只能出現(xiàn)在in位置上。

?這里有個生產(chǎn)者和消費者的概念記法,生產(chǎn)者只生產(chǎn)數(shù)據(jù)來輸出(將數(shù)據(jù)發(fā)送出去,output),而消費者只消費(接收傳進來的數(shù)據(jù),input)。

produce = output = out = 協(xié)變
consume = input = in = 逆變

?當(dāng)然了,out到協(xié)變以及in到逆變還需要你自己去聯(lián)系起來。
?有點汗流浹背了...但還是那句話,技術(shù)學(xué)習(xí)的道路沒有捷徑,重在積累。

參考:《第一行代碼》第三版 郭霖

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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