Kotlin核心編程 第五章類型系統

null引用

對於空指針異常,當前java已經有了如下處理:
1函數內對於無效值,更傾向於拋異常處理;
2採用@NotNull/@Nullable標註,明確參數是否可空,避免非法null值進一步傳遞;
3使用專門的optional對象對可能爲null的變量進行裝箱。這類對象必須拆箱後才能參與運算。
*java8新增了Optional<T>類

Kotlin的可空類型:?.
*由於null只能被存儲在java的引用類型的變量中,所以在kotlin中基本數據的可空版本都會使用該類型的包裝形式。同樣,如果你用基本數據類型作爲泛型類的類型參數,Kotlin同樣會使用該類型的包裝形式。
Elvis操作符:?:
非空斷言!!.

上面說到過解決npe一般有三種方式:
1用try catch捕獲異常
2用Optional<T>類似的類型來包裝
3用@NotNull/@Nullable註解來標註

kotlin是:在方法參數上標註了@Nullable,在實現上,依舊採用了if..else來對可空情況進行判斷。兼容了java老版本,實現與java100%互轉,性能上達到最佳。

如需拋出異常則爲:

val s :Student?= null
println(s?.glas?:throw NullPointerException("some thing null"))

類型檢查 is

    if(obj is String){
        pringln(obj.lenth)
    }

這裏的obj類型爲Any,是kotlin的智能轉換(Smart Casts)幫我們省略了一些工作。
Kotlin與其他面嚮對象語言一樣,無法直接將父類型轉爲子類型,當需要類型轉換時,利用as操作符來實現。
*Kotlin還提供了as?操作符

5.3比java更面向對象的設計

與Object作爲java類層級結構的頂層類似,Any類型時Kotlin中所有非空類型(如String,Int)的超類。

與Object作爲Java類層級結構的頂層類似,Any類型是 Kotlin 中所有非空類型的超類。
對於Kotlin 來說,如果定義了一個沒有指定父類型的類型,則該類型將是Any的直接子類型。如果定義了父類型,那麼該父類型是該類的直接父類型,但是新類型的最終根類型爲Any。
Kotlin的Type Checker強制檢查了父子關係。例如,你可以將子類型值存儲到父類型變量中。但是不能將父類型值存儲到子類型中。
Kotlin把Java方法參數和返回類型中用到的Object類型看作Any(更準確的說是“平臺類型”)。當在Kotlin函數中使用Any時,它會編譯成Java字節碼中的Object。

Any? 所有類型的根類型

Any是所有非空類型的根類型,Any?纔是所有類型(可空和非空類型)的根類型。

Any? 與Any??
如果Any?是Any的父類型,那麼Any?? 是否又是Any?的父類型,如果成立,是否意味着就沒有所謂的所有類型的根類型了?
在Kotlin 中,可空類型可以看作是所謂的Union Type,近似於數學中的並集。如果用類型的並集來表示Any?可寫爲Any U Null。
相應的Any??就可以表示爲Any U Null U Null,等價於Any U Null,即Any??等價於Any?。因此,說Any?是所有類型的根類型是沒有問題的。

Nothing與Nothing?
Nothing是沒有實例的類型。Nothing類型的表達式不會產生任何值。需要注意的是:任何返回值爲Nothing的表達式之後的語句都是無法執行的。Kotlin中retrun、thorw等(流程控制中與跳轉相關的表達式)返回值都爲Nothing。
Nothing對應的Nothing?,可以從字面上解釋爲:可空的空。與Any、Any?類似,所以Nothing?是Nothing的父類型,所以Nothing處於Kotlin類型層級結構的最低層。它只能包含一個值:null,本質上與null沒有區別。所以可以使用null作爲可空類型的值。

自動裝箱與拆箱

Koltin中沒有int、double、float、long等基本數據類型,取而代之的是引用類型包裝類Int、Float、Double、Long。
除了代表數值的類型,還有布爾(Boolean)、字符(Char)、字符串(String)及數組(Arry)。
但是隻能說Kotlin比Java更接近純面向對象的設計。
因爲:

Kotlin代碼

val x1:Int = 10
val x2:Int? =12

轉Java代碼

public final class TestIntDemoKt {
   private static final int x1 = 10;
   @Nullable
   private static final Integer x2 = 12;
 
   public static final int getX1() {
      return x1;
   }
 
   @Nullable
   public static final Integer getX2() {
      return x2;
   }
}

對應的字節碼
BIPUSH 10
 PUTSTATIC com/example/kotlindemo/anyclassdemo/TestIntDemoKt.x1 : I
 BIPUSH 12
 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

觀察上面代碼可以發現,Koltin中的Int在JVM中實際以int存儲(對應字節碼類型是I)
所以我們可以簡單的認爲:
Kotlin中Int類型等同於int。
Kotlin中Int?等同於Integer。

“新“的數組類型

Kotlin中數組的創建:

val funList0 = arrayOf<Int>()//長度爲0的數組
val funList= arrayOf(1,2,3) //初始化長度爲3的數組

Kotlin中Array並不是一個原生的數據結構,而是一種Array類,甚至可以將Kotlin中Array視作集合類的一部分。

由於Smart Casts,編譯器能夠隱式推斷出 funList元素類型。
在Kotlin中,還有一些實用的類,IntArray、CharArray、ShortArray等,分別對應Java中的int[]、char[]、short[]等。

val intArray = intArrayOf(1,2)

IntArray等並不是Array的子類,所以兩者創建的相同值的對象,並不是相同對象。

*由於Kotlin中對原始類有特殊的優化(主要體現在避免自動裝箱帶來的開銷),所以建議優先使用原始類型數組。

5.4泛型:讓類型更加安全

泛型是一種編譯時的安全檢測機制,它允許在定義類、接口、方法時使用類型參數,聲明的類型參數在使用時用具體的類型來替換。
衆所周知,Java 1.5引入泛型。那麼我們來思考一個問題,爲什麼Java一開始沒有引入泛型,而1.5版本卻引入泛型?先來看一個場景:

List stringList = new ArrayList();
stringList.add(new Double(2.0));
String str = (String)stringList.get(0);
執行結果:
>>> java.lang.ClassCastException:
 java.lang.Double cannot be cast to java.lang.String
    at javat.Rectangle.main(Rectangle.java:29)

因爲ArrayList底層是用一個Object類型的數組實現的,這種實現雖然能讓ArrayList變得更通用,但也會帶來弊端。比如上面例子中,我們不小心向原本應作爲String類型的List中添加了一個Double類型的對象,理想的情況下編譯器應該能夠提示錯誤,但事實上這段代碼能編譯通過,在運行時卻會報錯。這是一個非常糟糕的體驗,我們真正需要的是在代碼編譯的時候就能發現錯誤,而不是讓錯誤的代碼發佈到生產環境中。這便是泛型誕生的一個重要的原因。有了泛型後,我們可以這麼做:

List<String> stringList = new ArrayList<String>();
        stringList.add(new  Double(2.0));  //編譯時報錯,add(java.lang.String)無法適配add
            (java.lang.Double)

利用泛型代碼在編譯時期就能發現錯誤,防止在真正運行的時候出現ClassCastException。當然,泛型除了能幫助我們在編譯時期進行類型檢查外,還有很多其他好處,比如自動類型轉換。
繼續來看第一段代碼,在獲取List中的值的時候,我們進行了以下操作:

 String str = (String)stringList.get(0);

是不是感覺異常的煩瑣,明明知道里面存的是String類型的值,取值的時候還要進行類型強制轉換。但有了泛型之後,就可以利用下面這種方式實現:

        List<String> stringList = new ArrayList<String>();
        stringList.add("test");
        String str = stringList.get(0);

有了泛型之後,不僅在編譯的時候能進行類型檢查,在運行時還會自動進行類型轉換。而且通過引入泛型,增強上述功能的同時並沒有增加代碼的冗餘性。比如我們無須爲聲明一個類型安全的List而去創建StringList、DoubleList等類,只需在聲明List的同時指定參數類型即可。
總的來說,泛型有以下幾點優勢:
類型檢查,能在編譯時就幫你檢查出錯誤;
更加語義化,比如我們聲明一個List<String>,便可以知道里面存儲的是String對象,而不是其他對象;
自動類型轉換,獲取數據時不需要進行類型強制轉換;
能寫出更加通用化的代碼。

如何在Kotlin中使用泛型

假設現在我們有一個需求,定義一個find方法,傳入一個對象,若列表中存在該對象,則返回該對象,不存在則返回空。由於原有的集合類不存在這個方法,所以可以定義一個新的集合類,同樣也要聲明泛型。我們可以這麼做:

class SmartList<T> : ArrayList<T> () {
    fun find(t: T): T? {
        val index = super.indexOf(t)
        return if (index >= 0) super.get(index) else null
    }
}
fun main(args: Array<String>) {
    val smartList = SmartList<String>()
    smartList.add("one")
    println(smartList.find("one"))//輸出one
    println(smartList.find("two").isNullOrEmpty())//輸出true
}

發現,Kotlin定義泛型類的方式與我們在Java中所看到的類似。另外泛型類同樣還可以繼承另一個類,這樣我們就可以使用ArrayList中的屬性和方法了。
當然,除了定義一個新的泛型集合類外,我們還可以利用擴展函數來實現這種需求。由於擴展函數支持泛型的情況,所以我們可以這麼做:

fun <T> ArrayList<T>.find(t: T): T? {
    val index = this.indexOf(t)
    return if (index >= 0) this.get(index) else null
}
fun main(args: Array<String>) {

    val arrayList = ArrayList<String>()
    arrayList.add("one")
    println(arrayList.find("one"))//輸出one
    println(arrayList.find("two").isNullOrEmpty())//輸出true
}

利用擴展函數這種方式也非常簡潔,所以,當你只是需要對一個集合擴展功能的時候,使用擴展函數非常合適。

使用泛型時是否需要主動指定類型?
在Kotlin中,以下的方式不被允許:

val arrayList = ArrayList()

而在Java中卻可以這麼做,這主要是因爲泛型是Java 1.5版本才引入的,而集合類在Java早期版本中就已經有了。各種系統中已經存在大量的類似代碼:

List list = new ArrayList();

所以,爲了保證兼容老版本的代碼,Java允許聲明沒有具體類型參數的泛型類。而Kotlin是基於Java 6版本的,一開始就有泛型,不存在需要兼容老版本代碼的問題。所以,當你聲明一個空列表時,Kotlin需要你顯式地聲明具體的類型參數。當然,因爲Kotlin具有類型推導的能力,所以以下這種方式也是可行的:

val arrayList = arrayListOf("one", "two")

總的來說,使用泛型可以讓我們的代碼變得更加通用化,更加靈活。但有時過於通用靈活並不是一個好的選擇,比如現在我們創建一個類型,只允許添加指定類型的對象。接下來我們就來看看如何在Kotlin中約束類型參數。

類型約束:設定類型上界
假設現在有一個盤子,他可以放任何東西,在Kotlin中我們可以這麼做

class Plate<T>(val t :T)

突然你想把自己的盤子歸歸類,一些只放水果,一些放菜,我們定義Fruit類並聲明Apple和Banana類來繼承他

open class Fruit(val weight:Double)
class Apple(weight:Double):Fruit(weight)
class Banana(weight:Double):Fruit(weight)

在定義一個水果盤子

class FruitPlate<T:Fruit>(val t :T)

這裏的T只能是Fruit類及其子類型。

val applePlate = FruitPlate<Apple>(Apple(100.0))
val applePlate = FruitPlate(Apple(100.0))//簡化寫法

java中通過extends關鍵字,而kotlin使用:

class FruitPlate<T extends Fruit>{}

通過where關鍵字,實現對泛型參數類型添加多個約束條件。
假設一把刀只能切在地上的水果:

fun main() {
    cut(Watermelon(3.0))
    cut(Apple(2.0))//Type mismatch: inferred type is Apple but Ground was expected
}

interface Ground{}

open class Fruit(val weight:Double)

class Apple(weight:Double):Fruit(weight)
class Watermelon(weight:Double):Fruit(weight),Ground


fun <T> cut(t:T) where T:Fruit,T:Ground{
    print("cut me")
}

5.5泛型的背後:類型擦除

Java爲什麼無法聲明一個泛型數組(加入泛型後,運行時無法知道數組的類型,無法滿足數組協變的原則)
我們先來看一個簡單的例子,Apple是Fruit的子類,思考下Apple[]和Fruit[],以及List<Apple>和List<Fruit>是什麼關係呢?

   Apple[] appleArray = new Apple[10];
    Fruit[] fruitArray = appleArray; //允許
    fruitArray[0] = new Banana(0.5); //編譯通過,運行報ArrayStoreException
    List<Apple> appleList = new ArrayList<Apple>();
    List<Fruit> fruitList = appleList; //不允許

我們發現一個奇怪的現象,Apple[]類型的值可以賦值給Fruit[]類型的值,而且還可以將一個Banana對象添加到fruitArray,編譯器能通過。作爲對比,List<Friut>類型的值則在一開始就禁止被賦值爲List<Apple>類型的值,這其中到底有什麼不同呢?

其實這裏涉及一個關鍵點,數組是協變的,而List是不變的。簡單來說,就是Object[]是所有對象數組的父類,而List<Object>卻不是List<T>的父類。

在解釋爲什麼在Java中無法聲明泛型數組之前,我們先來看一下Java泛型的實現方式。Java中的泛型是類型擦除的,可以看作僞泛型,簡單來說,就是你無法在程序運行時獲取到一個對象的具體類型。我們可以用以下代碼來對比一下List<T>和數組:

    System.out.println(appleArray.getClass());
    System.out.println(appleList.getClass());
    // 運行結果
    class [Ljavat.Apple;
    class java.util.ArrayList

從上面的代碼我們可以知道,數組在運行時是可以獲取自身的類型,而List<Apple>在運行時只知道自己是一個List,而無法獲取泛型參數的類型。而Java數組是協變的,也就是說任意的類A和B,若A是B的父類,則A[]也是B[]的父類。但是假如給數組加入泛型後,將無法滿足數組協變的原則,因爲在運行時無法知道數組的類型。
Kotlin中的泛型機制與Java中是一樣的,所以上面的特性在Kotlin中同樣存在。比如通過下面的方式同樣無法獲取列表的類型:

    val appleList = ArrayList<Apple>()
    println(appleList.javaClass)

但不同的是,Kotlin中的數組是支持泛型的,當然也不再協變,也就是說你不能將任意一個對象數組賦值給Array<Any>或者Array<Any? >。在Kotlin中Any爲所有類的父類,下面是一個例子

    val appleArray = arrayOfNulls<Apple>(3)
    val anyArray: Array<Any? > = appleArray //不允許

我們已經知道了在Kotlin和Java中泛型是通過類型擦除來實現的,那麼這又是爲什麼呢?

向後兼容的罪

簡單來說,就是老版本的Java文件編譯後可以運行在新版本的JVM上。我們知道,Java一開始是沒有泛型的,那麼在Java 1.5之前,在程序中會出現大量的以下代碼:

ArrayList list = new ArrayList();  //沒有泛型

一般在沒有泛型的語言上支持泛型,一般有兩種方式,以集合爲例:

1全新設計一個集合框架(全新實現現有的集合類或者創造新的集合類),不保證兼容老的代碼,優點是不需要考慮兼容老的代碼,寫出更符合新標準的代碼;缺點是需要適應新的語法,更嚴重的是可能無法改造老的業務代碼;
2在老的集合框架上改造,添加一些特性,兼容老代碼的前提下,支持泛型。

很明顯,Java選擇了後種方式實現泛型,這也是有歷史原因的,主要有以下兩點原因:
1)在Java1.5之前已經有大量的非泛型代碼存在了,若不兼容它們,則會讓使用者抗拒升級,因爲他要付出大量的時間去改造老代碼;
2)Java曾經有過重新設計一個集合框架的教訓,比如Java 1.1到Java1.2過程中的Vector到ArrayList, HashTable到HashMap,引起了大量使用者的不滿。
所以,Java爲了填補自己埋下的坑,只能用一種比較彆扭的方式實現泛型,那便是類型擦除
那麼,爲什麼使用類型擦除實現泛型可以解決我們上面說的新老代碼兼容的問題呢?我們先來看一下下面兩行代碼編譯後的內容:

ArrayList list = new ArrayList(); //(1)
ArrayList<String> stringList = new ArrayList<String>(); //(2)
對應字節碼:
      0: new              #2                       // class java/util/ArrayList
      3: dup
      4: invokespecial #3                       // Method java/util/ArrayList."<init>":()V
      7: astore_1
      8: new              #2                       // class java/util/ArrayList
    11: dup
    12: invokespecial #3                       // Method java/util/ArrayList."<init>":()V
    15: astore_2

我們發現方式1和方式2聲明的ArrayList再編譯後的字節碼是完全一樣的,這也說明了低版本編譯的class文件在高版本的JVM上運行不會出現問題。既然泛型在編譯後是會擦除泛型類型的,那麼我們又爲什麼可以使用泛型的相關特性,比如類型檢查、類型自動轉換呢?
類型檢查是編譯器在編譯前就會幫我們進行類型檢查,所以類型擦除不會影響它。那麼類型自動轉換又是怎麼實現的呢?我們來看一個例子:

ArrayList<String> stringList = new ArrayList<String>();
String s = stringList.get(0);

這段代碼大家都應該很熟悉,get方法返回的值的類型就是List泛型參數的類型。來看一下ArrayList的get方法的源碼:

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];  //強制類型轉換
    }

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

我們發現,背後也是通過強制類型轉化來實現的。這點從編譯後的字節碼也可以得到驗證:

0: new            #2  // class java/util/ArrayList
3: dup
4: invokespecial #3  // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_0
10: invokevirtual #4  // Method java/util/ArrayList.get:(I)Ljava/lang/Object; 獲取的是Object
13: checkcast     #5  // class java/lang/String強制類型轉換
16: astore_2
17: return

所以可以得出結論,雖然Java受限於向後兼容的困擾,使用了類型擦除來實現了泛型,但它還是通過其他方式來保證了泛型的相關特性。

類型擦除的矛盾

通常情況下使用泛型我們並不在意它的類型是否是類型擦除,但是在有些場景,我們卻需要知道運行時泛型參數的類型,比如序列化/反序列化的時候。這時候我們應該怎麼辦?通過前面的學習相信你對Java和Kotlin的泛型實現原理已經有了一定的瞭解,既然編譯後會擦除泛型參數類型,那麼我們是不是可以主動指定參數類型來達到運行時獲取泛型參數類型的效果呢?我們試着對上面的例子的Plate進行一下改造:

    open class Plate<T>(val t : T, val clazz: Class<T>) {
        fun getType() {
            println(clazz)
        }
    }

    val applePlate = Plate(Apple(1.0), Apple::class.java)

    applePlate.getType()

    //結果
    class Apple

使用這種方式確實可以達到運行時獲取泛型類型參數的效果。但是這種方式也有限制,比如我們就無法獲取一個泛型的類型,比如

    val listType = ArrayList<String>::class.java  //不被允許
    val mapType = Map<String, String>::class.java  //不被允許

那麼,還有沒有另外的方式能獲取各種類型的信息呢?有,那就是利用匿名內部類。我們來看下面的一個例子:

    val list1 = ArrayList<String>()
    val list2 = object : ArrayList<String>(){} //匿名內部類
    println(list1.javaClass.genericSuperclass)

    println(list2.javaClass.genericSuperclass)
    //結果:
    java.util.AbstractList<E>
    java.util.ArrayList<java.lang.String>

不可思議,第2種方式竟然能在運行時知道這個list是一個什麼樣的類型。心細的讀者應該發現,list2聲明的其實是一個匿名內部類。關於如何在Kotlin中用object來聲明一個匿名內部類的相關知識可以回顧一下前面相應內容。那麼,爲什麼使用匿名內部類的這種方式能夠在運行時獲取泛型參數的類型呢?其實泛型類型擦除並不是真的將全部的類型信息都擦除,還是會將類型信息放在對應class的常量池中的。
Java將泛型信息存儲在哪裏?
可以參考以下網頁:Where are generic types stored in java class files?
所以,既然還存儲着相應的類型信息,那麼我們就能通過相應的方式來獲取這個類型信息。使用匿名內部類我們就可以實現這種需求。我們着手來設計一個能獲取所有類型信息的泛型類:

    import java.lang.reflect.ParameterizedType
    import java.lang.reflect.Type

    open class GenericsToken<T> {   //
        var type: Type = Any::class.java
        init {
            val superClass = this.javaClass.genericSuperclass
            type = (superClass as ParameterizedType).getActualTypeArguments()[0]
        }
    }

    fun main(args: Array<String>) {
        val gt = object : GenericsToken<Map<String, String>>(){}  //使用object創建一個匿名內部類
        println(gt.type)
    }

    //結果
    java.util.Map<java.lang.String, ? extends java.lang.String>

匿名內部類在初始化的時候就會綁定父類或父接口的相應信息,這樣就能通過獲取父類或父接口的泛型類型信息來實現我們的需求。你可以利用這樣一個類來獲取任何泛型的類型,我們常用的Gson也是使用了相同的設計。
Gson的TypeToken實現參考以下網址:https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/reflect/TypeToken.java
比如,我們在Kotlin中可以這樣使用Gson來進行泛型類的反序列化:

    val json = ...
    val rType = object : TypeToken<List<String>>() {}.type
    val stringList = Gson().fromJson<List<String>>(json, rType)

其實,在Kotlin中除了用這種方式來獲取泛型參數類型以外,還有另外一種方式,那就是內聯函數

使用內聯函數獲取泛型

Kotlin中的內聯函數在編譯的時候編譯器便會將相應函數的字節碼插入調用的地方,也就是說,參數類型也會被插入字節碼中,我們就可以獲取參數的類型了。有關內聯函數的內容可以看一下前面的相應章節。下面我們就用內聯函數來實現一個可以獲取泛型參數的方法:

inline fun <reified T> getType() {
    return T::class.java
}

使用內聯函數獲取泛型的參數類型非常簡單,只需加上reified關鍵詞即可。這裏的意思相當於,在編譯的會將具體的類型插入相應的字節碼中,那麼我們就能在運行時獲取到對應參數的類型了。所以,我們可以在Kotlin中改進Gson的使用方式:

    inline fun <reified T : Any> Gson.fromJson(json: String): T {  //對Gson進行擴展
        return Gson().fromJson(json, T::class.java)
    }
    //使用
    val json = ...
    val stringList = Gson().fromJson<List<String>>(json)

這裏利用了Kotlin的擴展特性對Gson進行了功能擴展,在不改變原有類結構的情況下新增方法,很多場景用Kotlin來實現便會變得更加優雅。有關擴展的相關內容會在第7章講解。
另外需要注意的一點是,Java並不支持主動指定一個函數是否是內聯函數,所以在Kotlin中聲明的普通內聯函數可以在Java中調用,因爲它會被當作一個常規函數;而用reified來實例化的參數類型的內聯函數則不能在Java中調用,因爲它永遠是需要內聯的。

爲什麼List<String>不能賦值給List<Object>

假如可以:

List<String> stringList = new ArrayList<String>();
List<Object> objList = stringList;//假設可以,編譯報錯
objList.add(Integer(1));
String str = stringList.get(0);//將會出錯

如果這樣,她將會和數組支持泛型一樣,不在保證類型安全,所以java不支持這種行爲。
但是Kotlin這裏我們發現一個奇怪的現象:

val stringList :List<String> = ArrayList<String>()
val anyList:List<Any> = stringList//編譯成功

在Kotlin中竟然能將List<String>賦值給List<Any>關鍵在於這兩個List不是同一種類型。

java中
public interface List<E> extends Collection<E> {
...
}
kotlin中
public interface List<out E> extends Collection<E> {
...
}

雖然都叫List,也同樣支持泛型,但是Kotlin 中的List 定義的泛型參數 前面多了一個 out 關鍵字。這個關鍵字就對這個List 的特性起到了很大作用。普通方式定義的泛型是不變的,簡單來說就是不管類型A和類型B 是什麼關係,Generic< A> 與 Generic< B>(Generic 代表泛型類) 都沒有任何關係。
比如在Java中String是Object 的 子類型,但是List< String> 並不是 List< Object> 的子類型。在Kotlin中泛型的原理是一樣的。但是,Kotlin的List 爲什麼允許List< String> 賦值給List< Any>呢?

一個支持協變的List

kotlin中,如果在定義泛型類和泛型方法的泛型參數前面加上 out 關鍵詞,說明這個泛型類及泛型方法是協變的。類型A 是 類型 B的子類型,那麼Generic< A> 也是 Generic< B> 的子類型。
因爲Kotlin的List支持協變,所以他無法添加元素,只能從裏面讀取內容;

val stringList:List<String> = ArrayList<String>()
stringList.add("kotlin")//編譯報錯,不允許!!

List 一旦創建 就不能再被修改。這便是將泛型聲明爲協變需要付出的代價。
結論:支持斜邊的List只可以讀取而不可以添加,否則不是類型安全的,違背泛型的初衷。

通常情況下,若一個泛型類Generic< out T> 支持協變,那麼它裏面的方法的參數類型就不能使用T 類型,因爲一個方法的參數不允許傳入參數父類型的對象,可能會導致錯誤。可以添加@UnsafeVariance 註解 來解除這個限制。

一個支持逆變的Comparator

逆變:類型A 是 類型B的子類型,但是Generic< B>反過來又是 Generic< A>的子類型。
加上現在需要對一個MutableList< Double>進行排序,利用其sortWith 方法,我們需要傳入一個比較器:

val doubleComparator = Comparator<Double>{
    d1,d2 -> d1.compareTo(d2)
}

fun main() {
    val doubleList =  mutableListOf(2.0,3.0)
    doubleList.sortWith(doubleComparator)
    for(i in doubleList){
        print("$i ")
    }
}

但是如果又需要對MutableList< Int>,MutableList< Long>等進行排序,那我們可能又需要定義不同的Comparator 。試想定義一個比較器,給這些列表用,這些數字類的共同父類是Number類。

val numberComparator = Comparator<Number>{
    num1,num2-> num1.toDouble().compareTo(num2.toDouble())
}

fun main() {
    val doubleList =  mutableListOf(2.0,3.0)
    doubleList.sortWith(numberComparator)
    for(i in doubleList){
        print("$i ")
    }
    println()
    val intList = mutableListOf(5,1)
    intList.sortWith(numberComparator)
    for(i in intList){
        print("$i ")
    }
}

結果是成功運行了,這說明是可以這樣做的。

public fun <T> kotlin.collections.MutableList<T>.sortWith(comparator: kotlin.Comparator<in T> /* = java.util.Comparator<in T> */): kotlin.Unit { /* compiled code */ }

這裏又出現了一個in 關鍵詞、和out類似,它也是泛型有個另一個特性——逆變:類型A 是 類型B的子類型,但是Generic< B>反過來又是 Generic< A>的子類型。

用out關鍵字聲明的泛型參數類型將不能作爲方法的參數類型,但是可以作爲方法的返回值類型。而in剛好相反。

協變和逆變

類型通配符代替泛型參數,Java中的泛型類型通配符爲"?",而Koltin中用"*"來表示類型通配符。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章