Kotlin 泛型(修訂版)

轉(zhuǎn)載文章年鸳,出處: https://blog.kotliner.cn/2017/06/26/kotlin-generics/

0. 引子

Kotlin 100% 與 Java 兼容莽红,所以拋開(kāi)語(yǔ)言表面上面的種種特質(zhì)之外雾鬼,背后的語(yǔ)言邏輯或者說(shuō)“靈魂”與 Java 總是想通的帕胆。本文只涉及 Kotlin Jvm辛掠,Kotlin Js憋他、Kotlin Native 的具體實(shí)現(xiàn)可能有差異宿崭。

最近一段時(shí)間在慕課網(wǎng)上發(fā)了一套 Kotlin 的入門視頻亲铡,涵蓋了基礎(chǔ)語(yǔ)法、面向?qū)ο笃隙摇⒏唠A函數(shù)奖蔓、DSL、協(xié)程等比較有特色的知識(shí)點(diǎn)讹堤,不過(guò)有朋友提出了疑問(wèn):這門課為什么不專門講講泛型吆鹤、反射和注解呢?

我最早聽(tīng)到這個(gè)問(wèn)題的時(shí)候洲守,反應(yīng)比較懵逼疑务,因?yàn)槲揖尤粵](méi)有感覺(jué)到 Kotlin 的反射、泛型特別是注解有專門學(xué)習(xí)的必要梗醇,因?yàn)樗麄兏?Java 實(shí)在是太像了知允。

實(shí)際上,從社區(qū)里面學(xué)習(xí) Kotlin 的朋友的反應(yīng)來(lái)看叙谨,大家大多對(duì)于函數(shù)式的寫法温鸽,DSL,協(xié)程這些內(nèi)容比較困惑唉俗,或者說(shuō)不太適應(yīng)嗤朴,這與大家的知識(shí)結(jié)構(gòu)是密切相關(guān)的,面向?qū)ο蟮臇|西大家很容易理解虫溜,因?yàn)榫湍敲袋c(diǎn)兒內(nèi)容雹姊,你懂了 C++ 的面向?qū)ο螅琂ava 的也很容易理解衡楞,Kotlin 的也就不在話下了吱雏;而你沒(méi)有接觸過(guò) Lua 的狀態(tài)機(jī),沒(méi)有接觸過(guò) Python 的推導(dǎo)式瘾境,自然對(duì)于協(xié)程也就會(huì)覺(jué)得比較陌生歧杏。

所以我想說(shuō)的是,泛型這東西迷守,只要你對(duì) Java 泛型有一定的認(rèn)識(shí)犬绒,Kotlin 的泛型基本可以直接用。那我們這篇文章要干嘛呢兑凿?只是做一個(gè)簡(jiǎn)單的介紹啦凯力,都很好理解的。

1. 真·泛型和偽·泛型

Java 的泛型大家肯定都知道了礼华,1.5 之后才加入的咐鹤,可以為類和方法分別定義泛型參數(shù),就像下面這樣:

public class Generics<T>{
    private T t;
    ...
    public <R> R getResult(){
        ...
    }
}

Kotlin 的寫法呢圣絮?完全一樣:

class Generics<T>{
    private val t: T
    ...
    fun <R> getResult(): R{
        ...
    }
}

Java/Kotlin 的泛型實(shí)現(xiàn)采用了類型擦除的方式祈惶,這與 C# 的實(shí)現(xiàn)不同,后者是真·泛型扮匠,前者是偽·泛型捧请。當(dāng)然這么說(shuō)是從運(yùn)行時(shí)的角度來(lái)看的,在編譯期棒搜,Java 的泛型對(duì)于語(yǔ)法的約束也是真實(shí)存在的血久,所以你愿意的話,也可以管 Java 的泛型叫做編譯期真·泛型帮非。

那么什么是真·泛型呢氧吐?我們給大家看一段 C# 的代碼:

using System;

public class Program{
    public static void Main(String[] args){
        testGeneric<string>();
    }
    public static void testGeneric<T>(){
        Console.WriteLine(typeof(T));
    }
}

testGeneric 的泛型參數(shù) string 可以在運(yùn)行時(shí)獲取到,儼然一個(gè)真實(shí)可用的類型啊末盔。下面是輸出的結(jié)果:

System.String

那偽·泛型呢筑舅?如果同樣的代碼放到 Java 或者 Kotlin 當(dāng)中,結(jié)果會(huì)怎樣呢陨舱?

public static <T> void testGenerics(){
    System.out.println(T.class);
}

這段代碼無(wú)法編譯翠拣,因?yàn)?T 是個(gè)泛型參數(shù),你不能用它去獲取 class 對(duì)象游盲。為了更清楚地說(shuō)明問(wèn)題误墓,我們看下下面的代碼:

public static <T> T testGenerics(){
    T t = null;
    return t;
}

編譯后的字節(jié)碼:

public static testGenerics()Ljava/lang/Object;
 L0
  LINENUMBER 13 L0
  ACONST_NULL
  ASTORE 0
 L1
  LINENUMBER 14 L1
  ALOAD 0
  ARETURN
 L2
  LOCALVARIABLE t Ljava/lang/Object; L1 L2 0
  // signature TT;
  // declaration: T
  MAXSTACK = 1
  MAXLOCALS = 1

我們看到蛮粮,編譯之后 T 變成了 Object,簡(jiǎn)單來(lái)說(shuō)就相當(dāng)于:

public static Object testGenerics(){
    Object t = null;
    return t;
}

這就是傳說(shuō)中的類型擦除了谜慌。而 Kotlin 在 JVM 之上然想,編譯之后也是字節(jié)碼,機(jī)制與 Java 是一樣的欣范。也正是因?yàn)檫@個(gè)原因变泄,我們?cè)谑褂?Gson 反序列化對(duì)象的時(shí)候除了制定泛型參數(shù),還需要傳入一個(gè) class :

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    ...
}

顯然 Gson 沒(méi)有辦法根據(jù) T 直接去反序列化恼琼。

下面我們說(shuō)一點(diǎn)兒不太一樣的妨蛹。在 Kotlin 當(dāng)中有一個(gè)關(guān)鍵字叫做 reified,還有一個(gè)叫做 inline晴竞,后者可以將函數(shù)定義為內(nèi)聯(lián)函數(shù)蛙卤,前者可以將內(nèi)聯(lián)函數(shù)的泛型參數(shù)當(dāng)做真實(shí)類型使用,我們先來(lái)看例子:

inline fun <reified T> Gson.fromJson(json: String): T{
    return fromJson(json, T::class.java)
}

這是一個(gè) Gson 的擴(kuò)展方法噩死,有了這個(gè)之后我們就無(wú)須在 Kotlin 當(dāng)中顯式的傳入一個(gè) class 對(duì)象就可以直接反序列化 json 了表窘。

這個(gè)會(huì)讓人感覺(jué)到有點(diǎn)兒迷惑,實(shí)際上由于是內(nèi)聯(lián)的方法調(diào)用甜滨,T 的類型在編譯時(shí)就可以確定的:

class Person(var id: Int, var name: String)

fun test(){
    val person: Person = Gson().fromJson("""{"id": 0, "name": "Jack" }""")
}

反編譯之后:

public static final void test() {
  Gson $receiver$iv = new Gson();
  String json$iv = "{\"id\": 0, \"name\": \"Jack\" }";
  Person person = (Person)$receiver$iv.fromJson(json$iv, Person.class);
}

注意乐严,在這里,inline 是必須的衣摩。

2. 型變

2.1 Java 的型變

如果 Parent 是 Child 的父類昂验,那么 List<Parent>List<Child> 的關(guān)系是什么呢?對(duì)于 Java 來(lái)說(shuō)艾扮,沒(méi)有關(guān)系既琴。

也就是說(shuō)下面的代碼是無(wú)法編譯的:

List<Number> numbers = new ArrayList<Integer>(); //ERROR!

不過(guò) numbers 中可以添加 Number 類型的對(duì)象,所以我添加個(gè) Integer 可以不呢泡嘴?可以的:

numbers.add(1);

那么我要想添加一堆 Integer 呢甫恩?用 addAll 是吧?注意看下 addAll 的簽名:

boolean addAll(Collection<? extends E> c);

這個(gè)泛型參數(shù)又是什么鬼酌予?如果我把這個(gè)簽名寫成下面這樣:

boolean addAll(Collection<E> c);

我想要在 numbers 當(dāng)中 addAll 一個(gè) ArrayList<Integer>磺箕,那就不可能了,因?yàn)槲覀冋f(shuō)過(guò)抛虫,ArrayList<Number>ArrayList<Integer> 是兩個(gè)不同的類型松靡,毛關(guān)系都沒(méi)有。

? extends E 其實(shí)就是使用點(diǎn)協(xié)變建椰,允許傳入的參數(shù)可以是泛型參數(shù)類型為 Number 子類的任意類型雕欺。

當(dāng)然,也有 ? super E 的用法,這表示元素類型為 E 及其父類屠列,這個(gè)通常也叫作逆變啦逆。

2.2 Kotlin 的型變

型變包括協(xié)變、逆變笛洛、不變?nèi)N夏志。

下面我們看看 Kotlin 是怎么支持這個(gè)特性的。Kotlin 支持聲明點(diǎn)型變撞蜂,我們直接看 Collection 接口的定義:

public interface Collection<out E> : Iterable<E> {
    ...
}

out E 就是型變的定義,表明 Collection 的元素類型是協(xié)變的侥袜,即 Collection<Number> 也是 Collection<Int> 的父類蝌诡。

而對(duì)于 MutableList 來(lái)說(shuō),它的元素類型就是不變的:

public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {
    ...
    public fun addAll(elements: Collection<E>): Boolean
    ...
}

換言之枫吧,MutableCollection<Number>MutableCollection<Int> 沒(méi)有什么關(guān)系浦旱。

那么請(qǐng)注意看 addAll 的聲明,參數(shù)是 Collection<E>九杂,而 Collection 是協(xié)變的颁湖,所以傳入的參數(shù)可以是任意 E 或者其子類的集合。

逆變的寫法也簡(jiǎn)單一些: Collection<in E>例隆。

那么 Kotlin 是否支持使用點(diǎn)型變呢甥捺?當(dāng)然支持。

我們剛才說(shuō) MutableCollection 是不變的镀层,那么如果下面的參數(shù)改成這樣:

public fun addAll(elements: MutableCollection<E>): Boolean

結(jié)果就是镰禾,當(dāng) E 為 Number 時(shí),addAll 無(wú)法接類受似 ArrayList<Int> 的參數(shù)唱逢。而為了接受這樣的參數(shù)吴侦,我們可以修改一下簽名:

public fun addAll(elements: MutableCollection<out E>): Boolean

這其實(shí)就與 Java 的型變完全一致了。

2.3 @UnsafeVariance

型變是一個(gè)讓人費(fèi)解的話題坞古,很多人接觸這東西的時(shí)候一開(kāi)始都會(huì)比較暈备韧,我們來(lái)看看下面的例子:

class MyCollection<out T>{
    fun add(t: T){ // ERROR!
        ...
    }
}

為什么會(huì)報(bào)錯(cuò)呢?因?yàn)?T 是協(xié)變的痪枫,所以外部傳入的參數(shù)類型如果是 T 的話织堂,會(huì)出問(wèn)題,不信你看:

var myList: MyCollection<Number> = MyCollection<Int>()
myList.add(3.0)

上面的代碼毫無(wú)疑問(wèn)可以編譯奶陈,但運(yùn)行時(shí)就會(huì)比較尷尬捧挺,因?yàn)?MyCollection<Int> 希望接受的是 Int,沒(méi)想到來(lái)了一個(gè) Double尿瞭。

對(duì)于協(xié)變的類型闽烙,通常我們是不允許將泛型類型作為傳入?yún)?shù)的類型的,或者說(shuō),對(duì)于協(xié)變類型黑竞,我們通常是不允許其涉及泛型參數(shù)的部分被改變的捕发。這也很容易解釋為什么 MutableCollection 是不變的,而 Collection 是協(xié)變的很魂,因?yàn)樵?Kotlin 當(dāng)中扎酷,前者是可被修改的,后者是不可被修改的遏匆。

逆變的情形正好相反法挨,即不可以將泛型參數(shù)作為方法的返回值。

但實(shí)際上有些情況下幅聘,我們不得已需要在協(xié)變的情況下使用泛型參數(shù)類型作為方法參數(shù)的類型:

public interface Collection<out E> : Iterable<E> {
    ...
    public operator fun contains(element: @UnsafeVariance E): Boolean
    ...
}

比如這種情形凡纳,為了讓編譯器放過(guò)一馬,我們就可以用 @UnsafeVariance 來(lái)告訴編譯器:“我知道我在干啥帝蒿,保證不會(huì)出錯(cuò)荐糜,你不用擔(dān)心”。

最后再給大家提一個(gè)點(diǎn)葛超,現(xiàn)在你們知道為什么 in 表示逆變暴氏,out 表示協(xié)變了嗎?

3. * 投影

在Java 中绣张,當(dāng)我們不知道泛型具體類型的時(shí)候可以用 答渔?來(lái)代替具體的類型來(lái)使用,比如下面的寫法:

Class<?> cls = numbers.getClass();

Kotlin 也可以有類似的寫法:

val cls: Class<*> = list.javaClass
val cls2: Class<*> = List::class.java

Kotlin 可以根據(jù) * 所指代的泛型參數(shù)進(jìn)行相應(yīng)的映射侥涵,下面是官方的說(shuō)法:

  • 對(duì)于 Foo <out T>研儒,其中 T 是一個(gè)具有上界 TUpper 的協(xié)變類型參數(shù),Foo <*> 等價(jià)于 Foo <out TUpper>独令。 這意味著當(dāng) T 未知時(shí)端朵,你可以安全地從 Foo <*> 讀取 TUpper 的值。
  • 對(duì)于 Foo <in T>燃箭,其中 T 是一個(gè)逆變類型參數(shù)冲呢,Foo <*> 等價(jià)于 Foo <in Nothing>。 這意味著當(dāng) T 未知時(shí)招狸,沒(méi)有什么可以以安全的方式寫入 Foo <*>敬拓。
  • 對(duì)于 Foo <T>,其中 T 是一個(gè)具有上界 TUpper 的不型變類型參數(shù)裙戏,Foo<*> 對(duì)于讀取值時(shí)等價(jià)于 Foo<out TUpper> 而對(duì)于寫值時(shí)等價(jià)于 Foo<in Nothing>乘凸。

那么 * 在哪些場(chǎng)合下可以或者不可以使用呢?

我們來(lái)看幾個(gè)例子:

val list = ArrayList<*>()// ERROR!

* 不允許作為函數(shù)和變量的類型的泛型參數(shù)累榜!

fun <T> hello(args: Array<T>){
    ...
}

...
hello<*>(args) // ERROR!!

* 不允許作為函數(shù)和變量的類型的泛型參數(shù)营勤!

interface Foo<T>

class Bar : Foo<*> // ERROR!

* 不能直接作為父類的泛型參數(shù)傳入灵嫌!

interface Foo<T>

class Bar : Foo<Foo<*>>

這是正確的。注意葛作,盡管 * 不能直接作為類的泛型參數(shù)寿羞,Foo<*> 卻可以,按照前面官方給出的說(shuō)法赂蠢,它在讀時(shí)等價(jià)于Foo<out Any> 寫時(shí)等價(jià)于 Foo<in Nothing>

fun hello(args: Array<*>){
    ...
}

同樣绪穆,這表示接受的參數(shù)的類型在讀寫時(shí)分別等價(jià)于Array<out Any>Array<in Nothing>

4. 其他

4.1 Raw 類型

Raw 類型就是對(duì)于定義時(shí)有泛型參數(shù)要求,但在使用時(shí)指定泛型參數(shù)的情況虱岂,這個(gè)只在 Java 中有玖院,顯然也是為了前向兼容而已。

例如:

List list = new ArrayList();

這類用法在 Kotlin 當(dāng)中是不被允許的第岖。上面的代碼大致相當(dāng)于:

val list = ArrayList<Any?>()

不過(guò)难菌,在 Java 中,raw 類型可以有這種寫法:

List<Integer> integers = new ArrayList<>();
List list = new ArrayList();
list = integers;

但 Kotlin 中绍傲,單純的 ArrayList<Any?> 并不是協(xié)變的扔傅,所以下面的寫法是錯(cuò)誤的:

var list = ArrayList<Any?>()
val integers = ArrayList<Int>()
list = integers // ERROR!

Java耍共,你這樣做很危險(xiǎn)呀烫饼。

4.2 泛型邊界

在 Java 中,我們同樣可以用 extends 為泛型參數(shù)指定上限:

class NumberFormatter<T extends Number>{
    ...
}

這表示使用時(shí)试读,泛型參數(shù)必須是 Number 及其子類的一種杠纵。

而在 Kotlin 中,寫法與繼承類似:

class NumberFormatter<T: Number>{
    ...
}

如果有多個(gè)上界钩骇,那么:

class NumberFormatter<T> where T: Number, T: Cloneable{
    ...
}

5. 小結(jié)

通過(guò)上面的討論比藻,其實(shí)大家會(huì)發(fā)現(xiàn) Kotlin 的泛型相比 Java 有了更嚴(yán)格的約束,更簡(jiǎn)潔的表述倘屹,更靈活的配置银亲,但背后的思路和具體的實(shí)現(xiàn)總體來(lái)說(shuō)是一致的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纽匙,一起剝皮案震驚了整個(gè)濱河市务蝠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烛缔,老刑警劉巖馏段,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異践瓷,居然都是意外死亡院喜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門晕翠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)喷舀,“玉大人,你說(shuō)我怎么就攤上這事≡” “怎么了梯影?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)庶香。 經(jīng)常有香客問(wèn)我甲棍,道長(zhǎng),這世上最難降的妖魔是什么赶掖? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任感猛,我火速辦了婚禮,結(jié)果婚禮上奢赂,老公的妹妹穿的比我還像新娘陪白。我一直安慰自己,他們只是感情好膳灶,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布咱士。 她就那樣靜靜地躺著,像睡著了一般轧钓。 火紅的嫁衣襯著肌膚如雪序厉。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天毕箍,我揣著相機(jī)與錄音弛房,去河邊找鬼。 笑死而柑,一個(gè)胖子當(dāng)著我的面吹牛文捶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播媒咳,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼粹排,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了涩澡?” 一聲冷哼從身側(cè)響起顽耳,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筏养,沒(méi)想到半個(gè)月后斧抱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渐溶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年辉浦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茎辐。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宪郊,死狀恐怖掂恕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弛槐,我是刑警寧澤懊亡,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站乎串,受9級(jí)特大地震影響店枣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叹誉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一鸯两、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧长豁,春花似錦钧唐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至酸舍,卻和暖如春帅韧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背父腕。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工弱匪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留青瀑,地道東北人璧亮。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斥难,于是被迫代替她去往敵國(guó)和親枝嘶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容