轉(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ō)是一致的。