0. 引子
Kotlin 100% 與 Java 兼容,所以拋開語言表面上面的種種特質(zhì)之外,背后的語言邏輯或者說“靈魂”與 Java 總是想通的。本文只涉及 Kotlin Jvm,Kotlin Js、Kotlin Native 的具體實現(xiàn)可能有差異。
最近一段時間在慕課網(wǎng)上發(fā)了一套 Kotlin 的入門視頻,涵蓋了基礎(chǔ)語法、面向?qū)ο蟆⒏唠A函數(shù)、DSL、協(xié)程等比較有特色的知識點,不過有朋友提出了疑問:這門課為什么不專門講講泛型、反射和注解呢?
我最早聽到這個問題的時候,反應(yīng)比較懵逼,因為我居然沒有感覺到 Kotlin 的反射、泛型特別是注解有專門學(xué)習(xí)的必要,因為他們跟 Java 實在是太像了。
實際上,從社區(qū)里面學(xué)習(xí) Kotlin 的朋友的反應(yīng)來看,大家大多對于函數(shù)式的寫法,DSL,協(xié)程這些內(nèi)容比較困惑,或者說不太適應(yīng),這與大家的知識結(jié)構(gòu)是密切相關(guān)的,面向?qū)ο蟮臇|西大家很容易理解,因為就那么點兒內(nèi)容,你懂了 C++ 的面向?qū)ο?,Java 的也很容易理解,Kotlin 的也就不在話下了;而你沒有接觸過 Lua 的狀態(tài)機,沒有接觸過 Python 的推導(dǎo)式,自然對于協(xié)程也就會覺得比較陌生。
所以我想說的是,泛型這東西,只要你對 Java 泛型有一定的認識,Kotlin 的泛型基本可以直接用。那我們這篇文章要干嘛呢?只是做一個簡單的介紹啦,都很好理解的。
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 的泛型實現(xiàn)采用了類型擦除的方式,這與 C# 的實現(xiàn)不同,后者是真·泛型,前者是偽·泛型。當然這么說是從運行時的角度來看的,在編譯期,Java 的泛型對于語法的約束也是真實存在的,所以你愿意的話,也可以管 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 可以在運行時獲取到,儼然一個真實可用的類型啊。下面是輸出的結(jié)果:
System.String
那偽·泛型呢?如果同樣的代碼放到 Java 或者 Kotlin 當中,結(jié)果會怎樣呢?
public static <T> void testGenerics(){
System.out.println(T.class);
}
這段代碼無法編譯,因為 T 是個泛型參數(shù),你不能用它去獲取 class 對象。為了更清楚地說明問題,我們看下下面的代碼:
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,簡單來說就相當于:
public static Object testGenerics(){
Object t = null;
return t;
}
這就是傳說中的類型擦除了。而 Kotlin 在 JVM 之上,編譯之后也是字節(jié)碼,機制與 Java 是一樣的。也正是因為這個原因,我們在使用 Gson 反序列化對象的時候除了制定泛型參數(shù),還需要傳入一個 class :
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
...
}
顯然 Gson 沒有辦法根據(jù) T 直接去反序列化。
下面我們說一點兒不太一樣的。在 Kotlin 當中有一個關(guān)鍵字叫做 reified,還有一個叫做 inline,后者可以將函數(shù)定義為內(nèi)聯(lián)函數(shù),前者可以將內(nèi)聯(lián)函數(shù)的泛型參數(shù)當做真實類型使用,我們先來看例子:
inline fun <reified T> Gson.fromJson(json: String): T{
return fromJson(json, T::class.java)
}
這是一個 Gson 的擴展方法,有了這個之后我們就無須在 Kotlin 當中顯式的傳入一個 class 對象就可以直接反序列化 json 了。
這個會讓人感覺到有點兒迷惑,實際上由于是內(nèi)聯(lián)的方法調(diào)用,T 的類型在編譯時就可以確定的:
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)系是什么呢?對于 Java 來說,沒有關(guān)系。
也就是說下面的代碼是無法編譯的:
List<Number> numbers = new ArrayList<Integer>(); //ERROR!
不過 numbers 中可以添加 Number 類型的對象,所以我添加個 Integer 可以不呢?可以的:
numbers.add(1);
那么我要想添加一堆 Integer 呢?用 addAll 是吧?注意看下 addAll 的簽名:
boolean addAll(Collection<? extends E> c);
這個泛型參數(shù)又是什么鬼?如果我把這個簽名寫成下面這樣:
boolean addAll(Collection<E> c);
我想要在 numbers 當中 addAll 一個 ArrayList<Integer>,那就不可能了,因為我們說過,ArrayList<Number> 和 ArrayList<Integer> 是兩個不同的類型,毛關(guān)系都沒有。
? extends E 其實就是使用點協(xié)變,允許傳入的參數(shù)可以是泛型參數(shù)類型為 Number 子類的任意類型。
當然,也有 ? super E 的用法,這表示元素類型為 E 及其父類,這個通常也叫作逆變。
2.2 Kotlin 的型變
型變包括協(xié)變、逆變、不變?nèi)N。
下面我們看看 Kotlin 是怎么支持這個特性的。Kotlin 支持聲明點型變,我們直接看 Collection 接口的定義:
public interface Collection<out E> : Iterable<E> {
...
}
out E 就是型變的定義,表明 Collection 的元素類型是協(xié)變的,即 Collection<Number> 也是 Collection<Int> 的父類。
而對于 MutableList 來說,它的元素類型就是不變的:
public interface MutableCollection<E> : Collection<E>, MutableIterable<E> {
...
public fun addAll(elements: Collection<E>): Boolean
...
}
換言之,MutableCollection<Number> 與 MutableCollection<Int> 沒有什么關(guān)系。
那么請注意看 addAll 的聲明,參數(shù)是 Collection<E>,而 Collection 是協(xié)變的,所以傳入的參數(shù)可以是任意 E 或者其子類的集合。
逆變的寫法也簡單一些: Collection<in E>。
那么 Kotlin 是否支持使用點型變呢?當然支持。
我們剛才說 MutableCollection 是不變的,那么如果下面的參數(shù)改成這樣:
public fun addAll(elements: MutableCollection<E>): Boolean
結(jié)果就是,當 E 為 Number 時,addAll 無法接類受似 ArrayList<Int> 的參數(shù)。而為了接受這樣的參數(shù),我們可以修改一下簽名:
public fun addAll(elements: MutableCollection<out E>): Boolean
這其實就與 Java 的型變完全一致了。
2.3 @UnsafeVariance
型變是一個讓人費解的話題,很多人接觸這東西的時候一開始都會比較暈,我們來看看下面的例子:
class MyCollection<out T>{
fun add(t: T){ // ERROR!
...
}
}
為什么會報錯呢?因為 T 是協(xié)變的,所以外部傳入的參數(shù)類型如果是 T 的話,會出問題,不信你看:
var myList: MyCollection<Number> = MyCollection<Int>()
myList.add(3.0)
上面的代碼毫無疑問可以編譯,但運行時就會比較尷尬,因為 MyCollection<Int> 希望接受的是 Int,沒想到來了一個 Double。
對于協(xié)變的類型,通常我們是不允許將泛型類型作為傳入?yún)?shù)的類型的,或者說,對于協(xié)變類型,我們通常是不允許其涉及泛型參數(shù)的部分被改變的。這也很容易解釋為什么 MutableCollection 是不變的,而 Collection 是協(xié)變的,因為在 Kotlin 當中,前者是可被修改的,后者是不可被修改的。
逆變的情形正好相反,即不可以將泛型參數(shù)作為方法的返回值。
但實際上有些情況下,我們不得已需要在協(xié)變的情況下使用泛型參數(shù)類型作為方法參數(shù)的類型:
public interface Collection<out E> : Iterable<E> {
...
public operator fun contains(element: @UnsafeVariance E): Boolean
...
}
比如這種情形,為了讓編譯器放過一馬,我們就可以用 @UnsafeVariance 來告訴編譯器:“我知道我在干啥,保證不會出錯,你不用擔心”。
最后再給大家提一個點,現(xiàn)在你們知道為什么 in 表示逆變,out 表示協(xié)變了嗎?
3. * 投影
在Java 中,當我們不知道泛型具體類型的時候可以用 ?來代替具體的類型來使用,比如下面的寫法:
Class<?> cls = numbers.getClass();
Kotlin 也可以有類似的寫法:
val cls: Class<*> = list.javaClass
val cls2: Class<*> = List::class.java
Kotlin 可以根據(jù) * 所指代的泛型參數(shù)進行相應(yīng)的映射,下面是官方的說法:
- 對于
Foo <out T>,其中 T 是一個具有上界 TUpper 的協(xié)變類型參數(shù),Foo <*>等價于Foo <out TUpper>。 這意味著當 T 未知時,你可以安全地從Foo <*>讀取 TUpper 的值。 - 對于
Foo <in T>,其中 T 是一個逆變類型參數(shù),Foo <*>等價于Foo <in Nothing>。 這意味著當 T 未知時,沒有什么可以以安全的方式寫入Foo <*>。 - 對于
Foo <T>,其中 T 是一個具有上界 TUpper 的不型變類型參數(shù),Foo<*>對于讀取值時等價于Foo<out TUpper>而對于寫值時等價于Foo<in Nothing>。
那么 * 在哪些場合下可以或者不可以使用呢?
我們來看幾個例子:
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<*> 卻可以,按照前面官方給出的說法,它在讀時等價于Foo<out Any> 寫時等價于 Foo<in Nothing>
fun hello(args: Array<*>){
...
}
同樣,這表示接受的參數(shù)的類型在讀寫時分別等價于Array<out Any> 和 Array<in Nothing>
4. 其他
4.1 Raw 類型
Raw 類型就是對于定義時有泛型參數(shù)要求,但在使用時指定泛型參數(shù)的情況,這個只在 Java 中有,顯然也是為了前向兼容而已。
例如:
List list = new ArrayList();
這類用法在 Kotlin 當中是不被允許的。上面的代碼大致相當于:
val list = ArrayList<Any?>()
不過,在 Java 中,raw 類型可以有這種寫法:
List<Integer> integers = new ArrayList<>();
List list = new ArrayList();
list = integers;
但 Kotlin 中,單純的 ArrayList<Any?> 并不是協(xié)變的,所以下面的寫法是錯誤的:
var list = ArrayList<Any?>()
val integers = ArrayList<Int>()
list = integers // ERROR!
Java,你這樣做很危險呀。
4.2 泛型邊界
在 Java 中,我們同樣可以用 extends 為泛型參數(shù)指定上限:
class NumberFormatter<T extends Number>{
...
}
這表示使用時,泛型參數(shù)必須是 Number 及其子類的一種。
而在 Kotlin 中,寫法與繼承類似:
class NumberFormatter<T: Number>{
...
}
如果有多個上界,那么:
class NumberFormatter<T> where T: Number, T: Cloneable{
...
}
5. 小結(jié)
通過上面的討論,其實大家會發(fā)現(xiàn) Kotlin 的泛型相比 Java 有了更嚴格的約束,更簡潔的表述,更靈活的配置,但背后的思路和具體的實現(xiàn)總體來說是一致的。