Kotlin刨根問底(三):你真的懂泛型,會用嗎?

本文靈感來源:羣友提出泛型相關的問題,感受不少人對泛型並非很瞭解~
Kotlin中的泛型和Java中的泛型其實大同小異,只是語法稍微有些不一樣。
大部份內容摘取自:《Kotlin實用指南》,感興趣的能夠訂閱一波 ~java


0x一、要點提煉


  • 什麼是泛型 →「將肯定不變的類型參數化,保證集合中存儲元素都是同一種
  • 泛型的好處 →「編譯期類型安全檢查」和「消除強類型轉換,提升可讀性
  • Java中泛型的使用泛型類泛型接口泛型方法
  • Java假泛型實現原理 →「類型擦除(Type Erasure)」只存在與編譯期,進JVM前會擦除類型
  • Java有界泛型類型限制容許實例化的泛型類型,「extends 父類型
  • Java通配符<T>與<?>的區別(定義-形參實例化-實參)
  • 上邊界通配符<? extends 父類型>父類型未知,只能讀不能寫,協變
  • 下邊界通配符<? super 子類型>子類型未知,只能寫不能讀,逆變
  • 無限定通配符<?>,等價於<? extends Object>,類型徹底未知,只能讀不能寫,不變
  • Kotlin中的型變 →「聲明處型變out(協變)與in(逆變)類型映射<*>
  • Kotlin獲取泛型類型匿名內部類反射實例化類型參數代替類引用內聯函數

0x二、什麼是泛型



0x三、Java中泛型的使用


泛型可用於類、接口和方法的建立,對應:泛型類泛型接口泛型方法,代碼示例以下:web

注意事項安全

  • 一、泛型的參數只能是「類類型」,不能是簡單數據類型(int, float等);
  • 二、泛型能夠有多個泛型參數

Tips泛型類型的命名不是必須爲T,也可使用其餘「單個大寫字母」,沒有強制的命名規範,但爲了便於閱讀,有一些約定成俗的命名規範:app

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

0x四、Java假泛型實現原理


和C#中的泛型不一樣,Java和Kotlin中的泛型都是假泛型,實現原理就是「類型擦除(Type Erasure)」。
Java編譯器在生成Java字節碼中是不包含泛型中的類型信息的,只存在於代碼編譯階段,進JVM前會被擦除。
不信?寫個簡單的代碼體驗下:函數

運行結果以下學習

從輸出結果能夠看到得到的類型確實被擦除了,此時的「類類型」皆爲ArrayListspa

問 → 那咱們定義的泛型類型(Integer, String)到底去哪了?
答 → 被替換成了「原始類型」(字節碼中的真正類型)
問 → 那原始類型具體是什麼類型?
答 → 「限定類型」,無限定的話都用Object替換。
問 → ???
答 → 且聽我娓娓道來~.net

仍是上面的代碼,進Java字節碼看看(View -> Show Bytecode)3d

的確,Integer和String都被替換成了Object,那這個「限定類型」呢?寫個例子試試:code

看下字節碼

行吧,此時的「原始類型」爲「限定類型」即Animal類。

沒限定類型的,都替換成Object類,也使得咱們能夠經過一些操做,繞過泛型
好比,咱們利用「反射」往Integer類型的List插入一個String值是不會報錯的:

運行結果以下


0x五、Java有界泛型類型


若是不對泛型類型作限制,泛型類型可實例化爲任意類型的話,可能會產生某些安全隱患。爲了限制容許實例化的泛型類型,可在泛型類型後追加 extends 父類型,代碼示例以下:

有界泛型類型在必定程度上限制了泛型類型,提升了程序的安全性;由於定義了邊界,因此能夠調用父類或父接口的方法


0x六、Java通配符


Java泛型自己「不變」的,不支持「協變」和「逆變」,是經過通配符(?)來實現的


① <T>與<?>的區別

<T>泛型標識符」用於泛型「定義時」可理解爲「形參」;
<?>通配符」用於泛型「實例化時」可理解爲「實參」。

代碼示例以下

② 各類通配符

  • 上邊界通配符<? extends 父類型>
    實例化時可肯定爲「父類型的未知類型」故「只能讀不能寫」,從而使得類型是「協變的」。

代碼示例以下

  • 下邊界通配符:<? super 子類型>
    實例化時可肯定爲「子類型的未知類型」故「只能寫不能讀」從而使得類型是「逆變的」。
    (不能讀指的是不能按照泛型類型讀取)<代碼示例以下:

上邊界通配符只能讀不能寫,改成下邊界通配符

  • 無限定通配符:<?>
    等同於上邊界通配符<? extends Object>,「類型徹底未知」故「只能讀不能寫」從而使得類型是「不變的」。

Tips:看完這裏,估計有部分讀者仍是有點懵逼,這裏總結下:

  • 不變 → 子類和父類不要緊,正常存取,不用通配符就好,Java集合自己就是不變的;
  • 協變 → A是B的子類型,泛型<A>也是泛型<B>的子類型,只想取,用extends。
  • 逆變 → A是B的子類型,泛型<B>也是泛型<A>的子類型,只想存,用super。
  • PECS法則(Producer Extends,Consumer Super) 參數化類型是一個生產者,則使用:<? extends T> 若是它是一個消費者,則使用<? super T>

0x七、Kotlin中的型變


和Java泛型同樣,Kotlin中的泛型也是「不變的」,沒有「通配符類型」,但有兩個其餘的東西:「聲明處型變」(declaration-site variance)  與 「類型投影」(type projections)


① 聲明處型變


其實就是用「out」和「in」關鍵字來替換

  • out協變,等同於Java中的<? extends>,能讀不能寫,代碼示例以下:
  • in逆變,等同於Java中的<? super>,能寫不能讀,代碼示例以下:

② 類型投影

其實就是對應Java中的*通配符:

  • Java中<?>等同於<* extends Object>
  • Kotlin中<*>等同於out Any

0x七、Kotlin獲取泛型類型


在Kotlin中能夠經過下述四種方法獲取泛型的類型(前兩種Java也適用):

① 匿名內部類

原理:匿名內部類的聲明在編譯時進行,實例化在運行時進行代碼示例以下

② 反射

獲取運行時泛型參數類型,子類可得到父類泛型的具體類型。代碼示例以下

③ 實例化類型參數代替類引用

定義一個擴展函數用於啓動Activity,代碼示例以下:

fun <T: Activity> FragmentActivity.startActivity(context: Context, clazz: Class<T>) {
    startActivity(Intent(context, clazz))
}
// 調用
startActivity(context, MainActivity::class.java)
複製代碼

④ 內聯函數

Kotlin中使用「inline」關鍵字定義一個內聯函數,配合「reified」具體化(類型不擦除),獲得使用泛型類型的Class。修改後的代碼示例以下:

inline fun <reified T : Activity> Activity.startActivity(context: Context) {
    startActivity(Intent(context, T::class.java))
}
// 調用
startActivity<MainActivity>(context)
複製代碼


參考文獻

相關文章
相關標籤/搜索