深入理解Kotlin中的泛型(協變、逆變)

一、泛型的必要性

【1.1】沒有泛型之前

在說明爲什麼有泛型之前,我們先看一段代碼

List AList = new ArrayList();
//編譯通過,運行不報錯
A.add(new B());
//編譯通過,運行報錯
A a = (A) A.get(0);

這段代碼,現在已經很少看到了。但實際上在Java1.5之前,這是很經常寫的代碼,也很容易犯錯的代碼。在上面的代碼中,我們聲明瞭一個不知道儲存什麼類型的List。雖然我們通過變量名“AList”來代表這個List是存,取A類型的集合。但是我們仍然可以將B類型的對象存進去。而且取出來的時候,我們還需要進行類型強轉。這就帶來了兩個問題:

  1. 我們無法在儲存的時候,就限定輸入的類型。導致可能存入其他類型導致CastClassException。
  2. 集合元素取出來的時候,我們明明知道是A類型的,但是每次還是都要進行一次強轉。

出現這問題的原因根本在於,ArrayList()底層是使用Object[]實現的。這樣設計的本意是可以讓ArrayList更加的通用,適用於一切類型。

【1.2】有了泛型之後

在瞭解了上面的需求和痛點後,我們可以很自然的想起泛型。它可以讓類型參數化。在引入泛型後。上面的代碼我們可以這樣寫:

List<A> AList = new ArrayList();
//編譯不通過。
A.add(new B());
//不再需要強轉
A a = A.get(0);

可以看到,在引入了泛型後,在編譯時就能進行類型檢查。但是ArrayList底層實現還是使用Object[]的,爲什麼可以不用進行類型強轉呢? 我們可以看一下ArrayList.get()方法:

ArrayList.java

transient Object[] elementData;
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index]; //內部進行類型強轉
}

【1.3】泛型的必要性總結

到這裏,我們總結一下引入泛型的好處:

  1. 類型安全:編譯器可以在編譯時期就對類型錯誤的存取報錯。
  2. 類型參數化,可以寫出更加通用的代碼。
  3. 簡化代碼。
  4. 可以自動進行類型轉化,獲取數據是可以不用進行類型強轉

二、泛型的實現:類型擦除

【2.1】泛型的實際實現

其實關於泛型的背後實現,我們在上面有說到了一些。爲了更加深刻的體會他是通過類型擦除的方式來實現泛型的,我們看一下如下代碼的字節碼:

//沒有加泛型
ArrayList list = new ArrayList();
//加了泛型
ArrayList<A> AList = new ArrayList();

字節碼:

//ArrayList list = new ArrayList();
0:  new               #2
3:  dup
4:  invokespecial     #3
7:  astore+1
//ArrayList<A> AList = new ArrayList();
8:  new               #2
11: dup          
12: invokespecial     #3
15: astore_2

可以看到,ArrayList無論有沒有加泛型,它的字節碼都是一樣的。那麼它是怎麼保證我們在一所說的泛型帶來的特性呢?其實類型檢查可以通過編譯器檢查來實現。而類型自動轉化就如我們上面看到的一樣,通過泛型類內部強轉實現。

【2.2】爲什麼採用類型擦除實現泛型?

在瞭解了泛型的實現機制以後,我們反過來思考一下,Java爲什麼採用類型擦除的方式來實現泛型。答案是:向後兼容。 我們知道向後兼容是Java一強調的一大特性,而在Java1.5之前,還沒有出現泛型的時期,必然出現了大量如下代碼:

ArrayList list = new ArrayList();

而類型擦除的方式實現泛型,我們可以看到其編譯出來的字節碼,和1.5之前的是一樣的,可以說是完全兼容。然後泛型的一些特性通過編譯器和對現有集合框架類的改造實現。那Kotlin號稱是可以完全兼容Java的,所以Kotlin的泛型實現方式當然也是和Java一樣的了。

【2.3】泛型類型的獲取

通過上面中我們知道,爲了是提升代碼的通用型,我們使用泛型使類型參數化,抹去了不同類型帶來的差異。但是在我們編碼過程中,我們時常需要在運行中獲取對象類型,而經過類型擦除的泛型類,已經失去了類型參數的信息,那麼我們有什麼辦法可以運行中獲取這個類型參數嗎。或許我們可以通過手動指定的方式獲取。具體的代碼如下:

open class A<T>(val data: T, val clazz: Class<T>) {

    fun getType() {
        println(clazz)
    }

}

總結:這種方式獲取泛型類型參數難免麻煩了一點,而且它不能獲取一個獲取一個泛型類型。比如:

//編譯不同過,報錯
Class clazz = ArrayList<String>.class

那麼我們有沒有辦法獲取一個泛型類型呢,答案是有的:

【2.3.1】利用匿名內部類獲取泛型類型

val listA = new ArryaList<A>()
val listA2 = object : ArrayList<A>(){}

println(listA.javaClass.genericSuperclass)
println(lstA2.javaClass.genericSuperclass)

//打印:
java.util.AbstractList<E>
java.util.ArrayList<java.lang.String>

總結:我們發現,第二種我們可以獲取到list是一個什麼樣的類型。而第二種就是聲明瞭一個匿名內部類。但是爲什麼匿名內部類就能獲取到lis泛型參數的類型呢?其實類型擦除並不是真的將全部的類型信息都擦除了,還是會將類型信息放在對於的class的常量池中的。

所以我們可以嘗試設計出獲取所有類型信息的泛型類。

open class GenericsToken<T> {
    var type: Type = Any::class.java
    init {
        val superClass = this.javaClass.gnericSuperclass
        type = superClass as ParameterizedType).getActualTypeArguments()[0]
    }
}


fun test() {
    val gt = object : GenericsToken<Map<String, String>>(){}
    println(gt.type)
}

//打印結果
java.util.Map<java.lang.String, ? extends java.lang.String>

總結:匿名內部類在初始化的時候,綁定父類或父類接口的相應信息,這樣可以通過獲取父類或父藉口的父接口的泛型類型信息來獲取我們想要的泛型類型。其實常用的Gson框架也是採用這樣的方式獲取的。

val json = new Json("...")
val type = object : TypeToken<List<String>>(){}.type
val stringList = Gson().fromJson<List<String>>(json.type)

【2.3.2】使用 Kotlin 的 reified 關鍵字獲取泛型類型

我們知道Kotlin的內聯函數是在編譯的時候,編譯器把內聯函數的字節碼直接插入到調用的地方,所以參數類型也會被插入到字節碼中。而在內聯函數中獲取泛型的參數類型也非常簡單,只需要加上reified關鍵字就可以。

inline fun <reified T> getType(): T {
    return T::class.java
}

三、類型約束。

我們前面說的泛型時,講到其中一個特性就是類型安全,其實也就是說泛型本身帶有類型的約束力。那麼這裏講的類型約束是什麼意思呢。其實就是對泛型的約束。在Java中看我們會看到如下代碼:

class Test<T extends B> {
...    
}

通過在T後面加了extends B約束了這個泛型必須是B的子類。那麼在Kotlin中,繼承是用:表示的,所以Kotlin的泛型約束如下:

class Test<T: B>{
    
}

但是,如果我們需要多個約束呢?在Kotlin中可以使用 where 關鍵字來實現這個需求如下:

class Test<T> where T: A, T: B{

}

利用where關鍵字,我們可以約束泛型T必須是A和B的子類。

四、泛型的變形:協變和逆變

【4.1】協變

講義:如果類型A是類型B的子類型,那麼Generic<A>也是Generic<B>的子類,這就是協變。
在kotlin中,我們要實現這種關係,可以通過在泛型類或者泛型方法的泛型參數前面加 out 關鍵字。如下:

//定義實體類關係
open class Flower
class WhiteFlower: Flower(){}
class ReaFlower: Flower(){}

//生產者
interface Product<out T> {
     fun produce(): T
}

class WhiteFlowerProduct<WhiteFlower> {
    //將泛型類型作爲返回
    override fun produce(): WhiteFlower {
       return WhileFlower();
    }
}

//如下編譯通過
val product: Product<Flower> = WhiteFLowerProduct()

總結:可以看到,WhiteFLowerProduct()可以賦值給Product 類型變量,就是因爲通過out指明瞭協變關係。而且我們也看到,泛型類型做爲返回類型,被生產出來。那麼如果我們添加一個泛型類型的對象呢?如下:

interface Product<out T> {
    fun produce(): T
    //編譯器報錯
    fun add(t: T)
}

class WhiteFlowerProduct<WhiteFlower> {
    //將泛型類型作爲返回
    override fun produce(): WhiteFlower {
       return WhileFlower();
    }
    
    override fun add(flower: WhiteFlower){
       return WhileFlower();
    }
}

結果是編譯器報錯:Type parameter T is declare as 'out' but occurs in 'in' position in type T。翻譯過來就是被聲明爲out的類型T不能出現在輸入的位置。其實我們通過'out'關鍵字也可以知道,被其修飾的泛型只能作爲生產者輸出,而不能作爲消費者輸入。所以'out'修飾的泛型常常作爲方法的返回而使用。這就是協變帶來的限制。那麼協變爲什麼不能輸入呢。我們可以採用反證法來理解:假如可以添加,那麼會發生什麼事?

val flowerProduct: Product<Flower> = WhiteFLowerProduct()
//編譯不出錯,但是運行時會出現類型不兼容錯誤。
flowerProduct.add(ReaFlower())

其在Java中,相對應的泛型協變我們是這樣定義的:<? extends Object> 但是這一不便理解的泛型協變定義在Kotlin上被改進成用out關鍵字,更加能體現其協變只讀不可寫的特性。

【4.2】逆變

定義:如果類型A是類型B的子類型,反過來Generic<B>是Generic<A>的子類型,我們稱這種關係爲逆變。在Kotlin中,我們用'in'關鍵字來聲明逆變泛型。如下例子:

val numberComparator = Comparator<Number> {
    n1, n2 -> n1.toDouble.compareTo(n2.toDouble())
}

val daoubleList = mutableListOf(2.0, 3.0)
//針對Double數據類型,我們使用Number類型的Comparator
doubleList.sortWith(numberComparator)

val intList = mutableListof(1, 2)
//針對Int數據類型,我們仍然使用Number 類型的Comparator
intList.sortWith(numberComparator)

//可以看到這裏對泛型T,使用了in關鍵字。
public fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit {
    if (size > 1) java.util.Collections.sort(this, comparator)
}

通過如上代碼我們知道,本來Double和Int是Number的子類,通過in修飾符後,Comparator成爲了Comparator 和 Comparator 的子類,所以可以將Comparator賦值給Comparator和Comparator。從而不用在專門根據不同的數據類型,定義不同的DoubleComparator、IntComparatort等。 同樣的,通過它的名字'in'也可以知道。in修飾的泛型只能作爲輸入類型,而不能作爲返回類型。在Java中它對應着<? super T>。

【4.3】總結

另外,我分享一份從網絡上收錄整理的 Android架構視頻+BAT面試專題PDF+學習筆記,還有Android開發面試專題資料,高級進階架構資料供大家學習進階, 希望可以幫助到大家進入大廠、拿到高薪。

如果你現在有需要的話,可以在 石墨文檔 上查看《Android開發核心知識點筆記》最新版,路過別忘了點個Star

喜歡本文的話,不妨給我點個小贊、評論區留言或者轉發支持一下唄~

《Android開發核心知識點筆記》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章