Kotlin版本的WanAndroid項目實戰(zhàn)(五):Kotlin泛型與注解

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)總體來說是一致的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容