日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Kotlin泛型的型變之路演變示例詳解_Android

作者:xuyisheng ? 更新時間: 2023-01-23 編程語言

引言

之前就寫過一篇泛型的文章,但是總覺得寫得不夠系統,所以最近對泛型又作了些研究,算是對這篇文章的補充了。

kotlin之泛型

泛型,是為了讓「類」、「接口」、「方法」具有更加通用的使用范圍而誕生的,舉個例子,假如我們不使用泛型,那么一個List中可以裝得下任何對象,這么做的問題就在于,在使用時,需要對類型進行檢查,不然就會轉換異常。 所以,我們需要將這種檢查前置到編譯期,這樣在編寫代碼時,就可以安全的使用不同類型,例如List,我們一看就知道是一個String類型的list,不能放其他類型的元素。 在Java中,由于歷史原因,它并不存在真泛型,Java所有的泛型都是偽泛型,因為Java在編譯期,會執行「泛型擦除」,從而導致在Java字節碼中,不存在類型信息(但是類型會被保存在其它地方,這個后面講)。

正是由于泛型擦除的問題,你甚至可以通過反射繞開泛型的限制,傳遞一個非當前泛型限制的對象。

泛型類型在Java中,通常以一個大寫字母來進行標識,我們并不是一定要寫「T」來表示泛型,但這是一個約定成俗的表示,類似的約束還有下面這些。

  • 通用泛型類型:T,S,U,V
  • 集合元素泛型類型:E
  • 映射鍵-值泛型類型:K,V
  • 數值泛型類型:N

要理解Kotlin的泛型,我們最好首先從Java的泛型來學習,畢竟Kotlin的語法糖太多了,Java會更加白話文一點。 首先,Java中的泛型具有「不變性」,也就是說,編譯器會認為List和List是兩個完全不同的類型,當然,不僅僅是List,比如下面這個例子。

open class A
class B : A()

那么Test<A>和Test<B>是不是一個類型呢?必須不是,雖然A和B是父子關系,但Test<A>和Test<B>就不是了,為什么呢?我們站在編譯器的角度來想想,假如它們是同一個類型,那么在Test類中get出來的實例,到底是A還是B呢?所以編譯器為了避免這種不確定性,就否定了Test<A>和Test<B>是一種類型的推斷。 但是這種處理在我們處理泛型業務時,會有很多限制,所以,泛型提供了「型變」來拓展泛型的使用。

協變

協變指的是,當參數具有父子關系時,子類可以作為參數傳遞,而泛型的上界就是其父類。協變通過上界通配符<? extends 父類型>來實現。 實例化時可確定為「父類型的未知類型」,所以它「只能讀不能寫」,因為編譯器不確定到底是哪個子類。 例如下面的代碼。

List<Button> buttons = new ArrayList<Button>();
List<? extends TextView> textViews = buttons;

由于Button是TextView的子類,所以上面的代碼可以正確運行。我們來解釋下上面的代碼。

  • 「?」通配符表示這是一個未知類型
  • 「extends」上界通配符表示這個類型只能是其子類或者本身
  • 這里不僅可以是類,也可以適用于接口

上界通配符還有一個特例,那就是「?」,例如List<?>,實際上就是List<? extends Object>的縮寫。 在Kotlin中,使用的是「*」,即List<*>,實際上就是List<out Any>

簡而言之,協變就是——如果A是B的子類,那么Generic<A>就是Generic<? extends B>的子類型。

協變的限制

我們來看下面的代碼。

List<? extends TextView> textViews = new ArrayList<TextView>();
TextView textView = textViews.get(0);
// Error
textViews.add(textView);

我們來解釋下上面的代碼,首先,我們定義了一個具有泛型上界的list,然后,我們從list中讀取一個元素,這時候,這個元素的返回類型是什么呢編譯器并不知道,但由于泛型上限的存在,所以它一定是TextView及其子類,所以定義為TextView類型,也完全沒有問題。 接下來我們來實現寫入,這時候,就報錯了。

看上去好像沒錯啊,add進去的元素是TextView類型,符合泛型上界的定義啊,但是,這個List的類型定義是<?extends TextView>,編譯器并不知道具體是什么類型,所以它就認為,最好的辦法就是什么都不讓加,多做就是錯,那不如不做。 所以,經過協變之后的泛型,就失去了寫入的能力,它只能用于向外提供數據,也就是「數據生產者Producer」。

逆變

逆變指的是,父類可以作為參數傳遞,但子類必須是其下界。逆變通過下界通配符<? super 子類型>來實現。 實例化時可確定為「子類型的未知類型」,所以「只能寫不能讀」。

不能讀指的是不能讀取為指定的類型,而不是不能調用讀的方法。

例如下面的代碼。

List<? super Button> buttons = new ArrayList<TextView>();

同樣我們來分析下上面的代碼。

  • 「?」通配符表示這是一個未知類型
  • 「super」下界通配符表示后面的這個類型,只能是它子類或者本身
  • 這里不僅可以是類,也可以適用于接口

其實這整個就是協變的反向操作。一個是約束上界,另一個是約束下界,所以對比著,其實很好理解。 簡而言之,逆變就是——如果A是B的子類,那么Generic<B>就是Generic<? super A>的子類型。

逆變的限制

類似的,我們再來看下逆變的限制。

List<? super Button> buttons = new ArrayList<TextView>();
Button button = new Button(context);
buttons.add(button);
Object object = buttons.get(0);

上面的代碼,創建了一個list,它的元素類型的下界是Button,也就是說,這個list里面都是放的Button的父類類型。 所以,當我們創建一個Button,并寫入的時候,是完全可以的,因為它符合我們定義下界的約束。 再來看看讀取呢?當我們從list中讀取一個元素時,由于編譯器只知道它是Button的父類,但是具體是什么類型,它也不知道,所以,編譯器不如將它作為Object這個萬物基類了。 所以說,逆變之后的泛型,失去了讀的能力(因為讀出來都是Object),所以逆變泛型通常都作為「數據消費者Consumer」。

Kotlin型變

泛型讓我們有了可以支持多種類型的能力,型變讓我們有了修改泛型的能力,總結來說:

  • 泛型通配符<? extends x>可以使泛型支持協變,但是「只能讀不能寫」,這里的寫,指的是對泛型集合添加元素,如果是remove(int index)或者是clear這種刪除,則不受影響。
  • 泛型通配符<? super x>可以使泛型支持逆變,但是「只能寫不能讀」,這里的讀,指的是不能按照泛型類型讀,但如果按照Object讀出來再強轉具體類型,則是可以的。

在學習了Java泛型之后,我們再來看下Kotlin的泛型,這時候你再看,就沒那么復雜了,核心就兩條。

  • 使用關鍵字 out 來支持協變,等同于 Java 中的上界通配符 ? extends
  • 使用關鍵字 in 來支持逆變,等同于 Java 中的下界通配符 ? super

其實在理解了逆變和協變之后,你會發現out和in這兩個關鍵字真的是「言簡意賅」,out表示輸出,即協變只用于輸出數據,in表示輸入,即逆變只用于寫入數據。Kotlin官網上有個著名的——Consumer in, Producer out,說的就是這個意思。

Kotlin泛型的優化

我們通過這個例子來看下Kotlin對Java泛型的改進。

申明處型變

我們通過下面這個例子來看下Kotlin申明處型變的好處,這是一個生產者與消費者的例子,代碼如下。

// 生產者
class Producer<T> {
    fun produce(): T {}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce()

首先我們來看生產者,對于T類型的Producer,我們要創建它的子類時,就需要使用協變,即Producer,否則它就只能生產Button類型的數據。所以,在Java中,每次獲取數據的時候,都要聲明一次協變,所以Kotlin對其進行了優化,可以在申明處進行協變,代碼如下。

// 生產者
class Producer<out T> {
    fun produce(): T {}
}
val producer1: Producer<TextView> = Producer<Button>()
val producer2: Producer<out TextView> = Producer<Button>()

Kotlin約定,當泛型參數T只會用來輸出時,可以在申明類的時候,直接使用協變約束,這樣在調用的時候,就不用額外使用協變了,當然寫了也不會錯。 與此類似的,消費者也是如此。

// 消費者
class Consumer<T> {
    fun consume(t: T) {}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context))

我們在使用的時候,也是必須使用逆變,借助Kotlin,同樣可以在申明處進行逆變。

// 消費者
class Consumer<in T> {
    fun consume(t: T) {}
}
val consumer1: Consumer<Button> = Consumer<TextView>()
val consumer2: Consumer<in Button> = Consumer<TextView>()

這樣在調用的時候,就不用額外使用逆變了,當然寫了也不會錯。

reified

由于在Java會進行泛型擦除,所以編譯器無法在運行時知道一個確切的泛型類型,也就是說,我們無法在運行時,判斷一個對象是否為一個泛型T的實例,例如下面的代碼。

if (item instanceof T) {
    System.out.println(item);
}

同樣的,在Kotlin里面也是不行的,畢竟一母同胞。

if (item is T) {
    println(item)
}

為了解決這個問題,在Java或者Kotlin中,我們通常會多傳入一個Class類型的參數,然后通過Class.isInstance來判斷類型是否匹配。 但是由于Kotlin支持了內聯函數,所以它提供了一個更加方便的方式來處理這種場景,那就是「reified」配合「inline」來實現。

inline fun &lt;reified T&gt; checkType(item: Any) {
    if (item is T) {
        println(item)
    }
}

不是說好了不能直接對泛型來做類型判斷嗎,為什么這里卻可以呢?這其實就是內聯的作用,雖然這里是對T做判斷,但實際上在編譯時,這里已經被替換成了具體的類型,而不再是泛型T了,所以當然可以使用is來進行類型判斷了。

支持協變的List

在Kotlin中,有兩種List,一種是可變的,一種是不可變的,即MutableList和List,其中List的申明如下,它已經實現的協變,所以Kotlin中的List只能讀而不能寫。

public interface List&lt;out E&gt; : Collection&lt;E&gt;

獲取泛型的具體類型

reified

通過reified和inline配合,我們可以在運行時獲取泛型的具體類型,這是Kotlin的特性,具體的使用方式,上面的文章已經講了一個例子。下面我們再看看幾個比較典型的例子。

fun reifiedClass() {
    // normal
    val serviceImpl1 = ServiceLoader.load(Service::class.java)
    // reified
    val serviceImpl2 = loadService&lt;Service&gt;()
}
inline fun &lt;reified T&gt; loadService() {
    ServiceLoader.load(T::class.java)
}
interface Service {
    fun work()
}

再看一個簡化startActivity的方式。

inline fun &lt;reified T : Activity&gt; Activity.startActivity(bundle: Bundle? = null) {
    val intent = Intent(this, T::class.java)
    bundle?.let {
        intent.putExtras(it)
    }
    startActivity(intent)
}
startActivity&lt;SampleActivity&gt;()

傳入指定Class

通過傳入具體的Class類型,我們也可以在運行時獲取泛型類型,這個方法是Java和Kotlin都支持的,這個在前面的文章中也提到了。

匿名內部類

匿名內部類會在運行時實例化,這個時候,就可以拿到泛型的具體類型了,示例代碼如下。

open class Test<T>
fun main() {
    val innerClass = object : Test<String>() {}
    val genericType: Type? = innerClass.javaClass.genericSuperclass
    if (genericType is ParameterizedType) {
        val type = genericType.actualTypeArguments[0]
        // class java.lang.String
    }
}

Class類提供了一個方法getGenericSuperclass ,通過它可以獲取到帶泛型信息的父類Type(Java的Class文件會保留繼承的父類或者接口的泛型信息)。 通過對獲取的genericType來判斷是否實現ParameterizedType接口,是說明支持泛型,從而獲取出對應的泛型列表(因為泛型可能有多個)。 這個方式是一個很巧妙的獲取泛型類型的方法,在Gson中,就是通過它來獲取類型的。

val content = Gson().toJson("xxx", object : TypeToken<String>() {}.type)

在使用Gson時,我們需要創建一個繼承自TypeToken的匿名內部類, 并實例化泛型參數TypeToken,這樣我們就可以通過getGenericSuperclass來獲取父類的Type,也就是上面例子中的TypeToken了。

反射

反射自然是可以拿到運行時的具體類型了,代碼如下。

open class Test<T>
class NewTest : Test<String>() {
    private val genericType: Type? = javaClass.genericSuperclass
    fun test() {
        if (genericType is ParameterizedType) {
            val type = genericType.actualTypeArguments[0]
            // class java.lang.String
        }
    }
}

通過反射來獲取實際類型,是很大開源庫中都在使用的方式,例如Retrofit,它在內部就是通過method.genericReturnType來獲取泛型的返回類型,通過method.genericParameterTypes來獲取泛型的參數類型。

不過這里大家要好奇了,在文章的一開始,我們就說了,Java的偽泛型,會在編譯時進行泛型擦除,那么反射又是怎么拿到這些泛型信息的呢? 其實,編譯器還是留了一手,申明處的泛型信息,實際上會以Signature的形式,保存到Class文件的Constant pool中,這樣通過反射,就可以拿到具體的泛型類型了。

要注意的是,這里能保留的是申明處的泛型,如果是調用處的泛型,例如方法的傳參,這種就不會被保存了。

PESC

PESC是泛型型變中的一個指導性原則,意為「Producer Extend Consumer Super」,當然在Kotlin中,這句話要改為「Consumer in, Producer out」。 這個原則是從集合的角度出發的,其目的是為了實現集合的多態。

  • 如果只是從集合中讀取數據,那么它就是個生產者,可以使用extend
  • 如果只是往集合中增加數據,那么它就是個消費者,可以使用super
  • 如果往集合中既存又取,那么你不應該用extend或者super

還是舉一個例子來說明,我們可以認為Kotlin是Java的子類,但是List和List卻是兩個無關的類,它們之間沒有繼承關系,而使用List<? extends Java>后,相當于List和List之間也有了繼承關系,從而可以讀取List中不同類型的數據,List就是通過這種方式來實現了集合的多態。

協變和逆變的使用場景

我們來看這樣一段代碼,我們創建了一個copyAll的方法,傳入to和from兩個列表,代碼如下。

fun <T> copyAll(to: MutableList<T>, from: MutableList<T>) {
    to.addAll(from)
}
fun main() {
    val numberList = mutableListOf<Number>() // to
    val intList = mutableListOf(1, 2, 3, 4) // from
    copyAll(numberList, intList)// Error
}

但是這段代碼是不能編譯通過的,原因在于to是一個List,而from是一個List,所以類型轉換異常,不能編譯。 但實際上,我們知道Int是可以轉換為Number的,但是編譯器不知道,所以它只能報錯,編譯器需要的,就是我們告訴它,這樣做是安全的,得到了我們的保證,編譯器才能執行編譯。

這個保證是從兩方面來說的,首先我們來看from。 from是一個List,完全可以當做List,所以,要保證「from取出來的元素可以轉為Number類型,而且from不能再有其它寫入」,否則你向一個List中插入了一條Number類型的元素,那就不亂套了。 所以,我們可以對from做協變,讓它只讀不寫,代碼如下。

fun <T> copyAll(to: MutableList<T>, from: MutableList<out T>) {
    to.addAll(from)
}

這樣就表示from,只接受T或者T的子類型,也就是說,from只能是Number或者Number的子類型,而此時from是Int類型,所以編譯通過了。 上面是從from的角度做的保證,那么從to方面呢? 對于to來說,我們需要保證「to只能寫入,而不能讀取」。

fun <T> copyAll(to: MutableList<in T>, from: MutableList<T>) {
    to.addAll(from)
}

這樣就表示to,只接受T或者T的父類型,也就是說,to只能是Int或者Int的父類型,而此時to是Number類型,所以編譯通過了。

另外,我們將from的簽名改為List,也是可以編譯的,其原因就是Kotlin中的List已經支持協變了。

相信大家通過這個例子,大概能理解協變和逆變的使用方式了。 那么我們在實際的代碼中,要在哪些場景使用協變和逆變呢? 通常來說,泛型參數協變后則表示——「這個參數在當前類中,只能作為函數的返回值,或者是只讀屬性」。

abstract class TestOut<out T> {
    abstract val num: T// 只讀屬性
    abstract fun getItem(): T// 函數的返回值
    abstract var num1 : T// Error 用于可變屬性
    abstract fun addItem(t: T)// Error 用于函數的參數
}

而逆變,表示這個參數「只能作為函數的參數,或者修飾可變屬性」。

abstract class TestIn<in T> {
    abstract val num: T//Error 只讀屬性
    abstract fun getItem(): T//Error 函數的返回值
    abstract fun addItem(t: T)// 用于函數的參數
}

原文鏈接:https://juejin.cn/post/7179047508763476029

欄目分類
最近更新