欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時間:2022年12月21日 15:11:35   作者:xuyisheng  
這篇文章主要為大家介紹了Kotlin泛型的型變之路演變示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

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

kotlin之泛型

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

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

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

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

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

open class A
class B : A()

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

協(xié)變

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

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

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

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

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

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

協(xié)變的限制

我們來看下面的代碼。

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

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

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

逆變

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

不能讀指的是不能讀取為指定的類型,而不是不能調(diào)用讀的方法。

例如下面的代碼。

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

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

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

其實這整個就是協(xié)變的反向操作。一個是約束上界,另一個是約束下界,所以對比著,其實很好理解。 簡而言之,逆變就是——如果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);

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

Kotlin型變

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

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

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

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

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

Kotlin泛型的優(yōu)化

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

申明處型變

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

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

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

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

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

// 消費者
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>()

這樣在調(diào)用的時候,就不用額外使用逆變了,當(dāng)然寫了也不會錯。

reified

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

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

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

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

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

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

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

支持協(xié)變的List

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

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

獲取泛型的具體類型

reified

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

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都支持的,這個在前面的文章中也提到了。

匿名內(nèi)部類

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

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來判斷是否實現(xiàn)ParameterizedType接口,是說明支持泛型,從而獲取出對應(yīng)的泛型列表(因為泛型可能有多個)。 這個方式是一個很巧妙的獲取泛型類型的方法,在Gson中,就是通過它來獲取類型的。

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

在使用Gson時,我們需要創(chuàng)建一個繼承自TypeToken的匿名內(nèi)部類, 并實例化泛型參數(shù)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,它在內(nèi)部就是通過method.genericReturnType來獲取泛型的返回類型,通過method.genericParameterTypes來獲取泛型的參數(shù)類型。

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

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

PESC

PESC是泛型型變中的一個指導(dǎo)性原則,意為「Producer Extend Consumer Super」,當(dāng)然在Kotlin中,這句話要改為「Consumer in, Producer out」。 這個原則是從集合的角度出發(fā)的,其目的是為了實現(xiàn)集合的多態(tài)。

  • 如果只是從集合中讀取數(shù)據(jù),那么它就是個生產(chǎn)者,可以使用extend
  • 如果只是往集合中增加數(shù)據(jù),那么它就是個消費者,可以使用super
  • 如果往集合中既存又取,那么你不應(yīng)該用extend或者super

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

協(xié)變和逆變的使用場景

我們來看這樣一段代碼,我們創(chuàng)建了一個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,所以類型轉(zhuǎn)換異常,不能編譯。 但實際上,我們知道Int是可以轉(zhuǎn)換為Number的,但是編譯器不知道,所以它只能報錯,編譯器需要的,就是我們告訴它,這樣做是安全的,得到了我們的保證,編譯器才能執(zhí)行編譯。

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

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已經(jīng)支持協(xié)變了。

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

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

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

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

以上就是Kotlin泛型的型變之路演變示例詳解的詳細內(nèi)容,更多關(guān)于Kotlin 泛型型變的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • android設(shè)置adb自帶screenrecord錄屏命令

    android設(shè)置adb自帶screenrecord錄屏命令

    這篇文章主要介紹了android設(shè)置adb自帶screenrecord錄屏命令,需要的朋友可以參考下
    2018-11-11
  • Android 動態(tài)添加Fragment的實例代碼

    Android 動態(tài)添加Fragment的實例代碼

    這篇文章主要介紹了Android 動態(tài)添加Fragment的實例代碼的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下
    2016-08-08
  • Android基礎(chǔ)之startActivityForResult()的用法詳解

    Android基礎(chǔ)之startActivityForResult()的用法詳解

    這篇文章主要給大家介紹了Android中startActivityForResult()的用法,文中給出了詳細的介紹與示例代碼,相信對大家的理解和學(xué)習(xí)具有一定參考借鑒價值,有需要的朋友們下面來一起看看吧。
    2017-01-01
  • 詳解Android之圖片加載框架Fresco基本使用(二)

    詳解Android之圖片加載框架Fresco基本使用(二)

    本篇文章主要介紹了Android之圖片加載框架Fresco基本使用,可以實現(xiàn)進度條和圖片縮放等內(nèi)容,有興趣的可以了解一下。
    2016-12-12
  • Android 基于agora 開發(fā)視頻會議的代碼

    Android 基于agora 開發(fā)視頻會議的代碼

    這篇文章主要介紹了Android 基于agora 開發(fā)視頻會議,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-01-01
  • Android輔助權(quán)限的介紹和配置完整記錄

    Android輔助權(quán)限的介紹和配置完整記錄

    這篇文章主要給大家介紹了關(guān)于Android輔助權(quán)限的介紹和配置的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-01-01
  • Android 圖片緩存機制的深入理解

    Android 圖片緩存機制的深入理解

    這篇文章主要介紹了Android 圖片緩存機制的深入理解的相關(guān)資料,這里提供了實現(xiàn)實例幫助大家理解圖片緩存機制的知識,需要的朋友可以參考下
    2017-08-08
  • android實現(xiàn)RecyclerView列表單選功能

    android實現(xiàn)RecyclerView列表單選功能

    這篇文章主要為大家詳細介紹了android實現(xiàn)RecyclerView列表單選功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-07-07
  • Android?Recyclerview實現(xiàn)左滑刪除功能

    Android?Recyclerview實現(xiàn)左滑刪除功能

    這篇文章主要為大家詳細介紹了Android?Recyclerview實現(xiàn)左滑刪除功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • Android利用Sensor實現(xiàn)傳感器功能

    Android利用Sensor實現(xiàn)傳感器功能

    這篇文章主要為大家詳細介紹了Android利用Sensor實現(xiàn)傳感器功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-11-11

最新評論