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)的超類。

20200215120301780.png

與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中用"*"來表示類型通配符。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容