Kotlin 泛型 VS Java 泛型

建議先閱讀我的上一篇文章 -- Java 泛型

和 Java 泛型一樣铆惑,Kotlin 泛型也是 Kotlin 語(yǔ)言中較難理解的一個(gè)部分。Kotlin 泛型的本質(zhì)也是參數(shù)化類型送膳,并且提供了編譯時(shí)強(qiáng)類型檢查员魏,實(shí)際上也是偽泛型,和 Java 泛型類型一樣肠缨。這篇文章將介紹 Kotlin 泛型里中的重要概念逆趋,以及與 Java 泛型的對(duì)比。

1. 泛型類型與泛型函數(shù)

Kotlin 下泛型類型與泛型函數(shù)的寫法晒奕,與 Java 差不多,直接看下面的例子:

// 泛型類型
class Box<T> {
    var t: T? = null
}

// 泛型函數(shù)名斟,類型參數(shù)在函數(shù)名之前
object Util {
    fun <K, V> compare(p1: Pair<K, V>, p2: Pair<K, V>): Boolean {
         return p1.first == p1.first && p2.second == p2.second
    }
}

Kotlin 中泛型的類型參數(shù)如果可以推斷出來脑慧,例如從構(gòu)造函數(shù)的參數(shù)或者其他途徑,允許省略類型參數(shù):

val p1 = Pair(1, "1")
val p2 = Pair(2, "2")
Util.compare(p1, p2)

通過 Tools -> Kotlin -> Show Kotlin Bytecode, 然后點(diǎn)擊字節(jié)碼上面的 Decompile 出 Java 代碼可以看出與 Java 泛型的原理是一樣的砰盐,都進(jìn)行了類型擦除闷袒。

2. 泛型約束

Java 中可以通過有界類型參數(shù)來限制參數(shù)類型的邊界,Kotlin 下泛型約束也可以限制參數(shù)類型的上界:

fun <T: Comparable<T>> compare(t1: T, t2: T) = t1.compareTo(t2)

默認(rèn)的上界是Any?岩梳,是可空類型囊骤,如果確定為非空類型的話,應(yīng)該使用<T: Any>冀值。

泛型約束中的尖括號(hào)中只能指定一個(gè)上界也物,如果需要多個(gè)上界,需要一個(gè)單獨(dú)的 where 子句:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<Any>
        where T : Comparable<T>,
              T : CharSequence {
    return list.filter { it > threshold }
}

3. 使用處型變:類型投影

在 Java 泛型的通配符中有一個(gè)“Producer Extends, Consumer Super”原則列疗,簡(jiǎn)稱 PECS 原則:只讀類型使用上界通配符? extends T滑蚯,只寫類型使用下界通配符? super T。Kotlin 中提供了類似功能的兩個(gè)操作符outin抵栈,分別生產(chǎn)和消費(fèi)告材。

先看_Collection.kt中一個(gè)擴(kuò)展函數(shù):

public operator fun <T> Collection<T>.plus(elements: Array<out T>): List<T>

// list 為 ArrayList<Number> 類型
val list = arrayListOf<Number>(1, 2, 3)
// array 為 Array<Float> 類型
val array = arrayOf(1f, 2f)
val list1: List<Number> = list.plus(array)

所以Array<out T>相當(dāng)于 Java 中的Array<? extends T>,而Array<in T>相當(dāng)于 Java 中的Array<? super T>古劲,out 表示生產(chǎn)斥赋,用于只讀類型,in 表示消費(fèi)产艾,用于只寫類型疤剑。

類型投影和 Java 的上界通配符和下界通配符一樣滑绒,只能用于參數(shù)、屬性骚露、局部變量或返回值的類型蹬挤,但是不能用于泛型類型和泛型函數(shù)聲明的類型,所以稱之使用處型變棘幸。

4. 聲明處型變

與 Java 有界通配符不能用于泛型聲明時(shí)使用不同的是焰扳,Kotlin 中outin兩個(gè)型變注解還可以用于泛型聲明時(shí),更加靈活误续。下面通過 Java 和 Kotlin 中對(duì) Collection 的定義來分析:

// Java 中 Collection 的定義吨悍,元素是可讀寫的
public interface Collection<E> extends Iterable<E> { ... }
public interface List<E> extends Collection<E> { ... }

// Collections 的 copy 方法
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

// 但是下面聲明在 Java 中是不允許的
public interface IllegalList<? extends T> extends Collection<E> { ... }

但是 Kotlin 中可以通過聲明處型變(型變的概念在后面會(huì)詳細(xì)解釋)定義只讀的集合:

// A generic collection of elements. Methods in this interface support only read-only access to the collection
public interface Collection<out E> : Iterable<E>

// A generic collection of elements that supports adding and removing elements.
public interface MutableCollection<E> : Collection<E>, MutableIterable<E>

Collection<out E>使得 Collection 里面的元素是只讀的,也使得 Collection<Number>Collection<Int> 的父類蹋嵌,在 Kotlin 中稱 Collection 的元素類型是協(xié)變的育瓜。對(duì)于協(xié)變的類型,通常不允許泛型類型作為函數(shù)的傳入?yún)?shù)栽烂。

in型變注解可以使得元素類型是逆變的躏仇,只能被消費(fèi),與協(xié)變相反腺办,通常不允許泛型類型作為函數(shù)的返回值焰手,看Comparable的定義:

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    // 1.0 類型為 Double,是 Number 的子類型
    x.compareTo(1.0)
    // 因?yàn)?Comparable 只能被消費(fèi)怀喉,所以可以賦值給 Comoparable<Double> 的變量
    val y: Comparable<Double> = x
}

4.1 UnsafeVariance 注解

上面提到過對(duì)于協(xié)變的類型书妻,通常不允許泛型類型作為函數(shù)的傳入?yún)?shù),對(duì)于逆變類型躬拢,通常不允許泛型類型作為函數(shù)的返回值躲履,但是有時(shí)我們可以通過@UnsafeVariance 注解告訴 Kotlin 編譯器:“我保證不會(huì)干壞事”,例如 Collection 的 contains 函數(shù):

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

5. 星投影

使用泛型的過程中聊闯,如果參數(shù)類型未知時(shí)工猜,在 Java 中可以使用原始類型(Raw Types),但是 Java 的原始類型是類型不安全的:

ArrayList<String> list = new ArrayList<>(5);

ArrayList unkownList = list;
Object first = unkownList.get(0);
unkownList.add(1);  // warning: Unchecked call to 'add(E)'
unkownList.add("1"); // warning: Unchecked call to 'add(E)'

而在 Kotlin 中馅袁,在參數(shù)類型未知時(shí)域慷,可以用星投影來安全的使用泛型:

val list = ArrayList<Int>(5)
val unkownList: ArrayList<*> = list
val first: Any = unkownList[0]
unkownList.add(1)  // error
unkownList.add("1") // error

對(duì)于ArrayList<*>來說,因?yàn)椴恢谰唧w的參數(shù)類型汗销,對(duì)于add(e E)這種不安全的操作犹褒,Kotlin 編譯器會(huì)直接報(bào)錯(cuò),比 Java 的原始類型更安全弛针。

Kotlin 中具體的星投影語(yǔ)法如下:

  • 對(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í),沒有什么可以以安全的方式寫入Foo<*>诅病。

  • 對(duì)于Foo<T>哪亿,其中T是一個(gè)具有TUpper的不型變類型參數(shù),Foo<*>對(duì)于讀取值時(shí)等價(jià)于Foo<out TUpper>贤笆,而對(duì)于寫值時(shí)等價(jià)于Foo<in Nothing>蝇棉。

如果泛型類型具有多個(gè)類型參數(shù),則每個(gè)類型參數(shù)都可以單獨(dú)投影芥永。例如篡殷,如果類型被聲明為interface Function<in T, out U>,我們可以想象以下星投影:

  • Function<*, String>表示Function<in Nothing, String>

  • Function<Int, *>表示Function<Int, out Any?>

  • Function<*, *>表示Function<in Nothing, out Any?>

6. 型變的概念

在上面提到過使用處型變和聲明處型變埋涧,那具體型變指什么呢板辽?型變:是否允許對(duì)參數(shù)類型進(jìn)行子類型轉(zhuǎn)換。例如在 Java 中List<Integer>List<Number>沒有直接的類型關(guān)系棘催,就是說 Java 中的泛型是不可以直接型變的戳气。

為了提高代碼的靈活性,Java 中可以通配符在使用時(shí)實(shí)現(xiàn)型變巧鸭,例如void addNumbers(List<? super Number> list)方法中可以傳List<Integer>List<Integer>List<? super Number>的子類型麻捻,而 Integer 也是 Number 的子類型纲仍,這也稱之為協(xié)變。另外贸毕,List<Number>List<? super Integer>的子類型郑叠,和 Integer 與 Number 之間的類型關(guān)系相反,稱之為逆變明棍。

Kotlin 中outin操作符可以更簡(jiǎn)潔地實(shí)現(xiàn) Java 的使用處型變乡革,而且還支持聲明處型變,這也使得 Kotlin 中的泛型是可以直接型變的栅葡。

Kotlin 下協(xié)變:interface List<out E>装悲,List<Int>是``List<Number>`的子類型卓起。

Kotlin 下逆變:interface Comparable<in T>Comparable<Double>Comparable<Number>的父類型视粮。

7. 具體化的類型參數(shù)

Kotlin 與 Java 中泛型都會(huì)進(jìn)行類型擦除,泛型的具體類型在運(yùn)行時(shí)是未知的橙凳,例如在解析 json 字符串時(shí):

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

還必須傳泛型的 Class 類型蕾殴,不能直接使用T.class獲取類型笑撞,除非使用反射。

而在 Kotlin 中可以使用reified修飾符將內(nèi)聯(lián)函數(shù)的泛型類型當(dāng)作具體的類型來使用钓觉,不需要再額外傳一個(gè) class 對(duì)象:

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

對(duì)于具體化的類型參數(shù)茴肥,可以當(dāng)做一個(gè)普通的類型一樣,as!as操作符也可以使用荡灾。因?yàn)?Kotlin 編譯器會(huì)把內(nèi)聯(lián)函數(shù)的代碼插入到調(diào)用者的地方瓤狐,所以可以在編譯期就確定泛型的類型。需要注意的是卧晓,Kotlin 中的reified的內(nèi)聯(lián)函數(shù)不能被 Java 代碼調(diào)用芬首。

8. 小結(jié)

回顧 Kotlin 和 Java 中的泛型,Kotlin 泛型擴(kuò)展了 Java 中的泛型逼裆,添加了使用處型變和更安全的星投影郁稍,還支持具體化的類型參數(shù)。我整理了下面表格對(duì)比兩者:

Java 泛型 Java 中代碼示例 Kotlin 中代碼示例 Kotlin 泛型
泛型類型 class Box<T> class Box<T> 泛型類型
泛型方法 <K, V> boolean method(Pair<K, V> p) fun <K, V> function(p: Pair<K, V>) 泛型函數(shù)
有界類型參數(shù) class Box<T extends Comparable<T> class Box<T : Comparable<T>> 泛型約束
上界通配符 void sumOfList(List<? extends Number> list) fun sumOfList(list: List<out Number>) 使用處協(xié)變
下界通配符 void addNumbers(List<? super Integer> list) fun addNumbers(list: List<in Int>) 使用處逆變
interface Collection<out E> : Iterable<E> 聲明處協(xié)變
interface Comparable<in T> 聲明處逆變
原始類型 ArrayList unkownList = new ArrayList<String>(5) val unkownList: ArrayList<*> = ArrayList<Int>(5) 星投影

總的來說胜宇,Kotlin 泛型更加簡(jiǎn)潔安全耀怜,但是和 Java 一樣都是有類型擦除的,都屬于編譯時(shí)泛型桐愉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末财破,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子从诲,更是在濱河造成了極大的恐慌左痢,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件系洛,死亡現(xiàn)場(chǎng)離奇詭異俊性,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)描扯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門定页,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绽诚,你說我怎么就攤上這事典徊。” “怎么了恩够?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵卒落,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我玫鸟,道長(zhǎng)导绷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任屎飘,我火速辦了婚禮妥曲,結(jié)果婚禮上贾费,老公的妹妹穿的比我還像新娘。我一直安慰自己檐盟,他們只是感情好褂萧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葵萎,像睡著了一般导犹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上羡忘,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天谎痢,我揣著相機(jī)與錄音,去河邊找鬼卷雕。 笑死节猿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的漫雕。 我是一名探鬼主播滨嘱,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼浸间!你這毒婦竟也來了太雨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤魁蒜,失蹤者是張志新(化名)和其女友劉穎囊扳,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兜看,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宪拥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铣减。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脚作,死狀恐怖葫哗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情球涛,我是刑警寧澤劣针,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站亿扁,受9級(jí)特大地震影響捺典,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜从祝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一襟己、第九天 我趴在偏房一處隱蔽的房頂上張望引谜。 院中可真熱鬧,春花似錦擎浴、人聲如沸员咽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)贝室。三九已至,卻和暖如春仿吞,著一層夾襖步出監(jiān)牢的瞬間滑频,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工唤冈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留峡迷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓务傲,卻偏偏與公主長(zhǎng)得像凉当,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子售葡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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

  • 前言 人生苦多看杭,快來 Kotlin ,快速學(xué)習(xí)Kotlin挟伙! 什么是Kotlin楼雹? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,146評(píng)論 9 118
  • 寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻尖阔,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,421評(píng)論 1 3
  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分介却,“神秘”的通配符谴供,讓我看了幾遍《Java...
    珞澤珈群閱讀 7,770評(píng)論 12 51
  • 本文是在學(xué)習(xí)和使用kotlin時(shí)的一些總結(jié)與體會(huì),一些代碼示例來自于網(wǎng)絡(luò)或Kotlin官方文檔齿坷,持續(xù)更新... 對(duì)...
    竹塵居士閱讀 3,269評(píng)論 0 8
  • 我是一只孤獨(dú)的鳥 我想有一個(gè)男朋友桂肌。 可我不敢和別人談戀愛。 我確實(shí)害怕寂寞永淌,更害怕失去崎场。 夜那么深那么涼。我想逼...
    小阿徐閱讀 362評(píng)論 13 4