建議先閱讀我的上一篇文章 -- 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è)操作符out
和in
抵栈,分別生產(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 中out
和in
兩個(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 中out
和in
操作符可以更簡(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í)泛型桐愉。