一篇文章弄懂Java和Kotlin的泛型難點

Java 和 Kotlin 的泛型算作是一塊挺大的知識難點瞭,涉及到很多很難理解的概念:泛型型參、泛型實參、類型參數、不變、型變、協變、逆變、內聯等等。本篇文章就將 Java 和 Kotlin 結合著一起講,按照我的個人理解來闡述泛型的各個知識難點,希望對你有所幫助 😇😇

一、泛型類型

泛型允許你定義帶類型形參的數據類型,當這種類型的實例被創建出來後,類型形參便被替換為稱為類型實參的具體類型。例如,對於 List<T>,List 稱為基礎類型,T 便是類型型參,T 可以是任意類型,當沒有指定 T 的具體類型時,我們隻能知道List<T>是一個集合列表,但不知道承載的具體數據類型。而對於 List<String>,當中的 String 便是類型實參,我們可以明白地知道該列表承載的都是字符串,在這裡 String 就相當於一個參數傳遞給瞭 List,在這語義下 String 也稱為類型參數

此外,在 Kotlin 中我們可以實現實化類型參數,在運行時的內聯函數中拿到作為類型實參的具體類型,即可以實現 T::class.java,但在 Java 中卻無法實現,因為內聯函數是 Kotlin 中的概念,Java 中並不存在

二、為什麼需要泛型

泛型是在 Java 5 版本開始引入的,先通過幾個小例子來明白泛型的重要性

以下代碼可以成功編譯,但是在運行時卻拋出瞭 ClassCastException。瞭解 ArrayList 源碼的同學就知道其內部是用一個Object[]數組來存儲數據的,這使得 ArrayList 能夠存儲任何類型的對象,所以在沒有泛型的年代開發者一不小心就有可能向 ArrayList 存入瞭非期望值,編譯期完全正常,等到在運行時就會拋出類型轉換異常瞭

public class GenericTest {

    public static void main(String[] args) {
        List stringList = new ArrayList();
        addData(stringList);
        String str = (String) stringList.get(0);
    }

    public static void addData(List dataList) {
        dataList.add(1);
    }

}
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

而有瞭泛型後,我們就可以寫出更加健壯安全的代碼,以下錯誤就完全可以在編譯階段被發現,且取值的時候也不需要進行類型強轉

    public static void main(String[] args) {
        List<String> stringList = new ArrayList();
        addData(stringList); //報錯
        String str = stringList.get(0);
    }

    public static void addData(List<Integer> dataList) {
        dataList.add(1);
    }

此外,利用泛型我們可以寫出更加具備通用性的代碼。例如,假設我們需要從一個 List 中篩選出大於 0 的全部數字,那我們自然不想為 Integer、Float、Double 等多種類型各寫一個篩選方法,此時就可以利用泛型來抽象篩選邏輯

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        integerList.add(-1);
        integerList.add(1);
        integerList.add(2);
        List<Integer> result1 = filter(integerList);

        List<Float> floatList = new ArrayList<>();
        floatList.add(-1f);
        floatList.add(1f);
        floatList.add(2f);
        List<Float> result2 = filter(floatList);
    }

    public static <T extends Number> List<T> filter(List<T> data) {
        List<T> filterList = new ArrayList<>();
        for (T datum : data) {
            if (datum.doubleValue() > 0) {
                filterList.add(datum);
            }
        }
        return filterList;
    }

總的來說,泛型有以下幾點優勢:

  • 類型檢查,在編譯階段就能發現錯誤
  • 更加語義化,看到 List<String>我們就知道存儲的數據類型是 String
  • 自動類型轉換,在取值時無需進行手動類型轉換
  • 能夠將邏輯抽象出來,使得代碼更加具有通用性

三、類型擦除

泛型是在 Java 5 版本開始引入的,所以在 Java 4 中 ArrayList 還不屬於泛型類,其內部通過 Object 向上轉型和外部強制類型轉換來實現數據存儲和邏輯復用,此時開發者的項目中已經充斥瞭大量以下類型的代碼:

List stringList = new ArrayList();
stringList.add("業志陳");
stringList.add("https://juejin.cn/user/923245496518439");
String str = (String) stringList.get(0);

而在推出泛型的同時,Java 官方也必須保證二進制的向後兼容性,用 Java 4 編譯出的 Class 文件也必須能夠在 Java 5 上正常運行,即 Java 5 必須保證以下兩種類型的代碼能夠在 Java 5 上共存且正常運行

List stringList = new ArrayList();
List<String> stringList = new ArrayList();

為瞭實現這一目的,Java 就通過類型擦除這種比較別扭的方式來實現泛型。編譯器在編譯時會擦除類型實參,在運行時不存在任何類型相關的信息,泛型對於 JVM 來說是透明的,有泛型和沒有泛型的代碼通過編譯器編譯後所生成的二進制代碼是完全相同的

例如,分別聲明兩個泛型類和非泛型類,拿到其 class 文件

public class GenericTest {

    public static class NodeA {

        private Object obj;

        public NodeA(Object obj) {
            this.obj = obj;
        }

    }

    public static class NodeB<T> {

        private T obj;

        public NodeB(T obj) {
            this.obj = obj;
        }

    }

    public static void main(String[] args) {
        NodeA nodeA = new NodeA("業志陳");
        NodeB<String> nodeB = new NodeB<>("業志陳");
        System.out.println(nodeB.obj);
    }

}

可以看到 NodeA 和 NodeB 兩個對象對應的字節碼其實是完全一樣的,最終都是使用 Object 來承載數據,就好像傳遞給 NodeB 的類型參數 String 不見瞭一樣,這便是類型擦除

public class generic.GenericTest {
  public generic.GenericTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class generic/GenericTest$NodeA
       3: dup
       4: ldc           #3                  // String 業志陳
       6: invokespecial #4                  // Method generic/GenericTest$NodeA."<init>":(Ljava/lang/Object;)V
       9: astore_1
      10: new           #5                  // class generic/GenericTest$NodeB
      13: dup
      14: ldc           #3                  // String 業志陳
      16: invokespecial #6                  // Method generic/GenericTest$NodeB."<init>":(Ljava/lang/Object;)V
      19: astore_2
      20: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_2
      24: invokestatic  #8                  // Method generic/GenericTest$NodeB.access$000:(Lgeneric/GenericTest$NodeB;)Ljava/lang/Object;
      27: checkcast     #9                  // class java/lang/String
      30: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      33: return
}

而如果讓 NodeA 直接使用 String 類型,並且為泛型類 NodeB 設定上界約束 String,兩者的字節碼也會完全一樣

public class GenericTest {

    public static class NodeA {

        private String obj;

        public NodeA(String obj) {
            this.obj = obj;
        }

    }

    public static class NodeB<T extends String> {

        private T obj;

        public NodeB(T obj) {
            this.obj = obj;
        }

    }

    public static void main(String[] args) {
        NodeA nodeA = new NodeA("業志陳");
        NodeB<String> nodeB = new NodeB<>("業志陳");
        System.out.println(nodeB.obj);
    }

}

可以看到 NodeA 和 NodeB 的字節碼是完全相同的

public class generic.GenericTest {
  public generic.GenericTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class generic/GenericTest$NodeA
       3: dup
       4: ldc           #3                  // String 業志陳
       6: invokespecial #4                  // Method generic/GenericTest$NodeA."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: new           #5                  // class generic/GenericTest$NodeB
      13: dup
      14: ldc           #3                  // String 業志陳
      16: invokespecial #6                  // Method generic/GenericTest$NodeB."<init>":(Ljava/lang/String;)V
      19: astore_2
      20: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_2
      24: invokestatic  #8                  // Method generic/GenericTest$NodeB.access$000:(Lgeneric/GenericTest$NodeB;)Ljava/lang/String;
      27: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      30: return
}

所以說,當泛型類型被擦除後有兩種轉換方式

  • 如果泛型沒有設置上界約束,那麼將泛型轉化成 Object 類型
  • 如果泛型設置瞭上界約束,那麼將泛型轉化成該上界約束

該結論也可以通過反射泛型類的 Class 對象來驗證

public class GenericTest {

    public static class NodeA<T> {

        private T obj;

        public NodeA(T obj) {
            this.obj = obj;
        }

    }

    public static class NodeB<T extends String> {

        private T obj;

        public NodeB(T obj) {
            this.obj = obj;
        }

    }

    public static void main(String[] args) {
        NodeA<String> nodeA = new NodeA<>("業志陳");
        getField(nodeA.getClass());
        NodeB<String> nodeB = new NodeB<>("https://juejin.cn/user/923245496518439");
        getField(nodeB.getClass());
    }

    private static void getField(Class clazz) {
        for (Field field : clazz.getDeclaredFields()) {
            System.out.println("fieldName: " + field.getName());
            System.out.println("fieldTypeName: " + field.getType().getName());
        }
    }

}

NodeA 對應的是 Object,NodeB 對應的是 String

fieldName: obj
fieldTypeName: java.lang.Object
fieldName: obj
fieldTypeName: java.lang.String

那既然在運行時不存在任何類型相關的信息,泛型又為什麼能夠實現類型檢查和類型自動轉換等功能呢?

其實,類型檢查是編譯器在編譯前幫我們完成的,編譯器知道我們聲明的具體的類型實參,所以類型擦除並不影響類型檢查功能。而類型自動轉換其實是通過內部強制類型轉換來實現的,上面給出的字節碼中也可以看到有一條類型強轉 checkcast 的語句

27: checkcast     #9                  // class java/lang/String

例如,ArrayList 內部雖然用於存儲數據的是 Object 數組,但 get 方法內部會自動完成類型強轉

transient Object[] elementData;

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

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

所以 Java 的泛型可以看做是一種特殊的語法糖,因此也被人稱為偽泛型

四、類型擦除的後遺癥

Java 泛型對於類型的約束隻在編譯期存在,運行時仍然會按照 Java 5 之前的機制來運行,泛型的具體類型在運行時已經被刪除瞭,所以 JVM 是識別不到我們在代碼中指定的具體的泛型類型的

例如,雖然List<String>隻能用於添加字符串,但我們隻能泛化地識別到它屬於List<?>類型,而無法具體判斷出該 List 內部包含的具體類型

List<String> stringList = new ArrayList<>();
//正常
if (stringList instanceof ArrayList<?>) {

}
//報錯
if (stringList instanceof ArrayList<String>) {

}

我們隻能對具體的對象實例進行類型校驗,但無法判斷出泛型形參的具體類型

public <T> void filter(T data) {
 //正常
 if (data instanceof String) {

 }
 //報錯
 if (T instanceof String) {

 }
 //報錯
 Class<T> tClass = T::getClass;
}

此外,類型擦除也會導致 Java 中出現多態問題。例如,以下兩個方法的方法簽名並不完全相同,但由於類型擦除的原因,入參參數的數據類型都會被看成 List<Object>,從而導致兩者無法共存在同一個區域內

public void filter(List<String> stringList) {

}

public void filter(List<Integer> stringList) {

}

五、Kotlin 泛型

Kotlin 泛型在大體上和 Java 一致,畢竟兩者需要保證兼容性

class Plate<T>(val t: T) {

    fun cut() {
        println(t.toString())
    }

}

class Apple

class Banana

fun main() {
    val plateApple = Plate<Apple>(Apple())
    //泛型類型自動推導
    val plateBanana = Plate(Banana())
    plateApple.cut()
    plateBanana.cut()
}

Kotlin 也支持在擴展函數中使用泛型

fun <T> List<T>.find(t: T): T? {
    val index = indexOf(t)
    return if (index > -1) get(index) else null
}

需要註意的是,為瞭實現向後兼容,目前高版本 Java 依然允許實例化沒有具體類型參數的泛型類,這可以說是一個對新版本 JDK 危險但對舊版本友好的兼容措施。但 Kotlin 要求在使用泛型時需要顯式聲明泛型類型或者是編譯器能夠類型推導出具體類型,任何不具備具體泛型類型的泛型類都無法被實例化。因為 Kotlin 一開始就是基於 Java 6 版本的,一開始就存在瞭泛型,自然就不存在需要兼容老代碼的問題,因此以下例子和 Java 會有不同的表現

val arrayList1 = ArrayList() //錯誤,編譯器報錯

val arrayList2 = arrayListOf<Int>() //正常

val arrayList3 = arrayListOf(1, 2, 3) //正常

還有一個比較容易讓人誤解的點。我們經常會使用 as 和 as? 來進行類型轉換,但如果轉換對象是泛型類型的話,那就會由於類型擦除而出現誤判。如果轉換對象有正確的基礎類型,那麼轉換就會成功,而不管類型實參是否相符。因為在運行時轉換發生的時候類型實參是未知的,此時編譯器隻會發出 “unchecked cast” 警告,代碼還是可以正常編譯的

例如,在以下例子中代碼的運行結果還符合我們的預知。第一個轉換操作由於類型相符,所以打印出瞭相加值。第二個轉換操作由於基礎類型是 Set 而非 List,所以拋出瞭 IllegalAccessException

fun main() {
    printSum(listOf(1, 2, 3)) //6
    printSum(setOf(1, 2, 3)) //IllegalAccessException
}

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> ?: throw IllegalAccessException("List is expected")
    println(intList.sum())
}

而在以下例子中拋出的卻是 ClassCastException,這是因為在運行時不會判斷且無法判斷出類型實參到底是否是 Int,而隻會判斷基礎類型 List 是否相符,所以 as? 操作會成功,等到要執行相加操作時才會發現拿到的是 String 而非 Number

printSum(listOf("1", "2", "3"))

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

六、上界約束

泛型本身已經帶有類型約束的作用,我們也可以進一步細化其支持的具體類型

例如,假設存在一個盤子 Plate,我們要求該 Plate 隻能用於裝水果 Fruit,那麼就可以對其泛型聲明做進一步約束,Java 中使用 extend 關鍵字來聲明約束規則,而 Kotlin 使用的是 : 。這樣 Plate 就隻能用於 Fruit 和其子類,而無法用於 Noodles 等不相關的類型,這種類型約束就被稱為上界約束

open class Fruit

class Apple : Fruit()

class Noodles

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

fun main() {
    val applePlate = Plate(Apple()) //正常
    val noodlesPlate = Plate(Noodles()) //報錯
}

如果上界約束擁有多層類型元素,Java 是使用 & 符號進行鏈式聲明,Kotlin 則是用 where 關鍵字來依次進行聲明

interface Soft

class Plate<T>(val t: T) where T : Fruit, T : Soft

open class Fruit

class Apple : Fruit()

class Banana : Fruit(), Soft

fun main() {
    val applePlate = Plate(Apple()) //報錯
    val bananaPlate = Plate(Banana()) //正常
}

此外,沒有指定上界約束的類型形參會默認使用 Any? 作為上界,即我們可以使用 String 或 String? 作為具體的類型實參。如果想確保最終的類型實參一定是非空類型,那麼就需要主動聲明上界約束為 Any

七、類型通配符 & 星號投影

假設現在有個需求,需要我們提供一個方法用於遍歷所有類型的 List 集合並打印元素

第一種做法就是直接將方法參數類型聲明為 List,不包含任何泛型類型聲明。這種做法可行,但編譯器會警告無法確定 list元素的具體類型,所以這不是最優解法

public static void printList1(List list) {
 for (Object o : list) {
  System.out.println(o);
 }
}

可能會想到的第二種做法是:將泛型類型直接聲明為 Object,希望讓其適用於任何類型的 List。這種做法完全不可行,因為即使 String 是 Object 的子類,但 List<String> 和 List<Object>並不具備從屬關系,這導致 printList2 方法實際上隻能用於List<Object>這一種具體類型

public static void printList2(List<Object> list) {
 for (Object o : list) {
  System.out.println(o);
 }
}

最優解法就是要用到 Java 的類型通配符 ? 瞭,printList3方法完全可行且編譯器也不會警告報錯

public static void printList3(List<?> list) {
 for (Object o : list) {
  System.out.println(o);
 }
}

? 表示我們並不關心具體的泛型類型,而隻是想配合其它類型進行一些條件限制。例如,printList3方法希望傳入的是一個 List,但不限制泛型的具體類型,此時List<?>就達到瞭這一層限制條件

類型通配符也存在著一些限制。因為 printList3 方法並不包含具體的泛型類型,所以我們從中取出的值隻能是 Object 類型,且無法向其插入值,這都是為瞭避免發生 ClassCastException

Java 的類型通配符對應 Kotlin 中的概念就是**星號投影 * **,Java 存在的限制在 Kotlin 中一樣有

fun printList(list: List<*>) {
    for (any in list) {
        println(any)
    }
}

此外,星號投影隻能出現在類型形參的位置,不能作為類型實參

val list: MutableList<*> = ArrayList<Number>() //正常

val list2: MutableList<*> = ArrayList<*>() //報錯

八、協變 & 不變

看以下例子。Apple 和 Banana 都是 Fruit 的子類,可以發現 Apple[] 類型的對象是可以賦值給 Fruit[] 的,且 Fruit[] 可以容納 Apple 對象和 Banana 對象,這種設計就被稱為協變,即如果 A 是 B 的子類,那麼 A[] 就是 B[] 的子類型。相對的,Object[] 就是所有數組對象的父類型

static class Fruit {

}

static class Apple extends Fruit {

}

static class Banana extends Fruit {

}

public static void main(String[] args) {
    Fruit[] fruitArray = new Apple[10];
    //正常
    fruitArray[0] = new Apple();
    //編譯時正常,運行時拋出 ArrayStoreException
    fruitArray[1] = new Banana();
}

而 Java 中的泛型是不變的,這意味著 String 雖然是 Object 的子類,但List<String>並不是List<Object>的子類型,兩者並不具備繼承關系

List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList; //報錯

那為什麼 Java 中的泛型是不變的呢?

這可以通過看一個例子來解釋。假設 Java 中的泛型是協變的,那麼以下代碼就可以成功通過編譯階段的檢查,在運行時就不可避免地將拋出 ClassCastException,而引入泛型的初衷就是為瞭實現類型安全,支持協變的話那泛型也就沒有比數組安全多少瞭,因此就將泛型被設計為不變的

List<String> strList = new ArrayList<>();
List<Object> objs = strList; //假設可以運行,實際上編譯器會報錯
objs.add(1);
String str = strList.get(0); //將拋出 ClassCastException,無法將整數轉換為字符串

再來想個問題,既然協變本身並不安全,那麼數組為何又要被設計為協變呢?

Arrays 類包含一個 equals方法用於比較兩個數組對象是否相等。如果數組是協變的,那麼就需要為每一種數組對象都定義一個 equals方法,包括開發者自定義的數據類型。想要避免這種情況,就需要讓 Object[] 可以接收任意數組類型,即讓 Object[] 成為所有數組對象的父類型,這就使得數組必須支持協變,這樣多態才能生效

public class Arrays {

     public static boolean equals(Object[] a, Object[] a2) {
        if (a==a2)
            return true;
        if (a==null || a2==null)
            return false;

        int length = a.length;
        if (a2.length != length)
            return false;

        for (int i=0; i<length; i++) {
            Object o1 = a[i];
            Object o2 = a2[i];
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }

        return true;
    }

}

需要註意的是,Kotlin 中的數組和 Java 中的數組並不一樣,Kotlin 數組並不支持協變,Kotlin 數組類似於集合框架,具有對應的實現類 Array,Array 屬於泛型類,支持瞭泛型因此也不再協變

val stringArray = arrayOfNulls<String>(3)
val anyArray: Array<Any?> = stringArray //報錯

Java 的泛型也並非完全不變的,隻是實現協變需要滿足一些條件,甚至也可以實現逆變,下面就來介紹下泛型如何實現協變和逆變

九、泛型協變

假設我們定義瞭一個copyAll希望用於 List 數據遷移。那以下操作在我們看來就是完全安全的,因為 Integer 是 Number 的子類,按道理來說是能夠將 Integer 保存為 Number 的,但由於泛型不變性,List<Integer>並不是List<Number>的子類型,所以實際上該操作將報錯

    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();

        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);

        copyAll(numberList, integerList); //報錯
    }

    private static <T> void copyAll(List<T> to, List<T> from) {
        to.addAll(from);
    }

思考下該操作為什麼會報錯?

編譯器的作用之一就是進行安全檢查並阻止可能發生不安全行為的操作,copyAll 方法會報錯,那麼肯定就是編譯器覺得該方法有可能會觸發不安全的操作。開發者的本意是希望將 Integer 類型的數據轉移到 NumberList 中,隻有這種操作且這種操作在我們看來肯定是安全的,但是編譯器不知道開發者最終所要做的具體操作啊

假設 copyAll方法可以正常調用,那麼copyAll方法自然隻會把 from 當做 List<Number>來看待。因為 Integer 是 Number 的子類,從 integerList 獲取到的數據對於 numberList 來說自然是安全的。而如果我們在copyAll方法中偷偷向 integerList 傳入瞭一個 Number 類型的值的話,那麼自然就將拋出異常,因為 from 實際上是 List<Integer>類型

為瞭阻止這種不安全的行為,編譯器選擇通過直接報錯來進行提示。為瞭解決報錯,我們就需要向編譯器做出安全保證:從 from 取出來的值隻會當做 Number 類型,且不會向 from 傳入任何值

為瞭達成以上保證,需要修改下 copyAll 方法

private static <T> void copyAll(List<T> to, List<? extends T> from) {
 to.addAll(from);
}

? extends T 表示 from 接受 T 或者 T 的子類型,而不單單是 T 自身,這意味著我們可以安全地從 from 中取值並聲明為 T 類型,但由於我們並不知道 T 代表的具體類型,寫入操作並不安全,因此編譯器會阻止我們向 from 執行傳值操作。有瞭該限制後,從integerList中取出來的值隻能是當做 Number 類型,且避免瞭向integerList插入非法值的可能,此時List<Integer>就相當於List<? extends Number>的子類型瞭,從而使得 copyAll 方法可以正常使用

簡而言之,帶 extends 限定瞭上界的通配符類型使得泛型參數類型是協變的,即如果 A 是 B 的子類,那麼 Generic<A> 就是Generic<? extends B>的子類型

十、泛型逆變

協變所能做到的是:如果 A 是 B 的子類,那麼 Generic<A> 就是Generic<? extends B>的子類型。逆變相反,其代表的是:如果 A 是 B 的子類,那麼 Generic<B> 就是 Generic<? super A> 的子類型

協變還比較好理解,畢竟其繼承關系是相同的,但逆變就比較反直覺瞭,整個繼承關系都倒過來瞭

逆變的作用可以通過相同的例子來理解,copyAll 方法如下修改也可以正常使用,此時就是向編譯器做出瞭另一種安全保證:向 numberList 傳遞的值隻會是 Integer 類型,且從 numberList 取出的值也隻會當做 Object 類型

private static <T> void copyAll(List<? super T> to, List<T> from) {
 to.addAll(from);
}

? super T表示 to 接收 T 或者 T 的父類型,而不單單是 T 自身,這意味著我們可以安全地向 to 傳類型為 T 的值,但由於我們並不知道 T 代表的具體類型,所以從 to 取出來的值隻能是 Object 類型。有瞭該限制後,integerList隻能向 numberList傳遞類型為 Integer 的值,且避免瞭從 numberList 中獲取到非法類型值的可能,此時List<Number>就相當於List<? super Integer>的子類型瞭,從而使得 copyAll 方法可以正常使用

簡而言之,帶 super 限定瞭下界的通配符類型使得泛型參數類型是逆變的,即如果 A 是 B 的子類,那麼 Generic<B> 就是 Generic<? super A> 的子類型

十一、out & in

Java 中關於泛型的困境在 Kotlin 中一樣存在,out 和 in 都是 Kotlin 的關鍵字,其作用都是為瞭來應對泛型問題。in 和 out 是一個對立面,同時它們又與泛型不變相對立,統稱為型變

  • out 本身帶有出去的意思,本身帶有傾向於取值操作的意思,用於泛型協變
  • in 本身帶有進來的意思,本身帶有傾向於傳值操作的意思,用於泛型逆變

再來看下相同例子,該例子在 Java 中存在的問題在 Kotlin 中一樣有

fun main() {
    val numberList = mutableListOf<Number>()

    val intList = mutableListOf(1, 2, 3, 4)

    copyAll(numberList, intList) //報錯

    numberList.forEach {
        println(it)
    }
}

fun <T> copyAll(to: MutableList<T>, from: MutableList<T>) {
    to.addAll(from)
}

報錯原因和 Java 完全一樣,因為此時編譯器無法判斷出我們到底是否會做出不安全的操作,所以我們依然要來向編譯器做出安全保證

此時就需要在 Kotlin 中來實現泛型協變和泛型逆變瞭,以下兩種方式都可以實現:

fun <T> copyAll(to: MutableList<T>, from: MutableList<out T>) {
    to.addAll(from)
}

fun <T> copyAll(to: MutableList<in T>, from: MutableList<T>) {
    to.addAll(from)
}

out 關鍵字就相當於 Java 中的<? extends T>,其作用就是限制瞭 from 不能用於接收值而隻能向其取值,這樣就避免瞭從 to 取出值然後向 from 賦值這種不安全的行為瞭,即實現瞭泛型協變

in 關鍵字就相當於 Java 中的<? super T>,其作用就是限制瞭 to 隻能用於接收值而不能向其取值,這樣就避免瞭從 to 取出值然後向 from 賦值這種不安全的行為瞭,即實現瞭泛型逆變

從這也可以聯想到,MutableList<*> 就相當於 MutableList<out Any?>瞭,兩者都帶有相同的限制條件:不允許寫值操作,允許讀值操作,且讀取出來的值隻能當做 Any?進行處理

十二、支持協變的 List

在上述例子中,想要實現協變還有另外一種方式,那就是使用 List

將 from 的類型聲明從 MutableList<T>修改為 List<T> 後,可以發現 copyAll 方法也可以正常調用瞭

fun <T> copyAll(to: MutableList<T>, from: List<T>) {
    to.addAll(from)
}

對 Kotlin 有一定瞭解的同學應該知道,Kotlin 中的集合框架分為兩種大類:可讀可寫和隻能讀不能寫

以 Java 中的 ArrayList 為例,Kotlin 將之分為瞭 MutableList 和 List 兩種類型的接口。而 List 接口中的泛型已經使用 out 關鍵字進行修飾瞭,且不包含任何傳入值並保存的方法,即 List 接口隻支持讀值而不支持寫值,其本身就已經滿足瞭協變所需要的條件,因此copyAll 方法可以正常使用

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

雖然 List 接口中有幾個方法也接收瞭 E 類型的入參參數,但該方法本身不會進行寫值操作,所以實際上可以正常使用,Kotlin 也使用 @UnsafeVariance抑制瞭編譯器警告

十三、reified & inline

上文講瞭,由於類型擦除,Java 和 Kotlin 的泛型類型實參都會在編譯階段被擦除,在 Kotlin 中存在一個額外手段可以來解決這個問題,即內聯函數

用關鍵字 inline 標記的函數就稱為內聯函數,再用 reified 關鍵字修飾內聯函數中的泛型形參,編譯器在進行編譯的時候便會將內聯函數的字節碼插入到每一個調用的地方,當中就包括泛型的類型實參。而內聯函數的類型形參能夠被實化,就意味著我們可以在運行時引用實際的類型實參瞭

例如,我們可以寫出以下這樣的一個內聯函數,用於判斷一個對象是否是指定類型

fun main() {
    println(1.isInstanceOf<String>())
    println("string".isInstanceOf<Int>())
}

inline fun <reified T> Any.isInstanceOf(): Boolean {
    return this is T
}

將以上的 Kotlin 代碼反編譯為 Java 代碼,可以看出來 main()方法最終是沒有調用 isInstanceOf 方法的,具體的判斷邏輯都被插入到瞭main()方法內部,最終是執行瞭 instanceof 操作,且指定瞭具體的泛型類型參數 String 和 Integer

public final class GenericTest6Kt {
   public static final void main() {
      Object $this$isInstanceOf$iv = 1;
      int $i$f$isInstanceOf = false;
      boolean var2 = $this$isInstanceOf$iv instanceof String;
      $i$f$isInstanceOf = false;
      System.out.println(var2);
      Object $this$isInstanceOf$iv = "string";
      $i$f$isInstanceOf = false;
      var2 = $this$isInstanceOf$iv instanceof Integer;
      $i$f$isInstanceOf = false;
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   // $FF: synthetic method
   public static final boolean isInstanceOf(Object $this$isInstanceOf) {
      int $i$f$isInstanceOf = 0;
      Intrinsics.checkNotNullParameter($this$isInstanceOf, "$this$isInstanceOf");
      Intrinsics.reifiedOperationMarker(3, "T");
      return $this$isInstanceOf instanceof Object;
   }
}

inline 和 reified 比較有用的一個場景是用在 Gson 反序列的時候。由於泛型運行時類型擦除的問題,目前用 Gson 反序列化泛型類時步驟是比較繁瑣的,利用 inline 和 reified 我們就可以簡化很多操作

val gson = Gson()

inline fun <reified T> toBean(json: String): T {
    return gson.fromJson(json, T::class.java)
}

data class BlogBean(val name: String, val url: String)

fun main() {
    val json = """{"name":"業志陳","url":"https://juejin.cn/user/923245496518439"}"""
    val listJson = """[{"name":"業志陳","url":"https://juejin.cn/user/923245496518439"},{"name":"業志陳","url":"https://juejin.cn/user/923245496518439"}]"""

    val blogBean = toBean<BlogBean>(json)
    val blogMap = toBean<Map<String, String>>(json)
    val blogBeanList = toBean<List<BlogBean>>(listJson)

    //BlogBean(name=業志陳, url=https://juejin.cn/user/923245496518439)
    println(blogBean)
    //{name=業志陳, url=https://juejin.cn/user/923245496518439}
    println(blogMap)
    //[{name=業志陳, url=https://juejin.cn/user/923245496518439}, {name=業志陳, url=https://juejin.cn/user/923245496518439}]
    println(blogBeanList)
}

我也利用 Kotlin 的這個強大特性寫瞭一個用於簡化 Java / Kotlin 平臺的序列化和反序列化操作的庫:JsonHolder

十四、總結

最後來做個簡單的總結

協變 逆變 不變
Kotlin <out T>,隻能作為消費者,隻能讀取不能添加 <in T>,隻能作為生產者,隻能添加,讀取出的值隻能當做 Any 類型 <T>,既可以添加也可以讀取
Java <? extends T>,隻能作為消費者,隻能讀取不能添加 <? super T>,隻能作為生產者,隻能添加,讀取出的值隻能當做 Object 類型 <T>,既可以添加也可以讀取

到此這篇關於Java和Kotlin的泛型難點的文章就介紹到這瞭,更多相關Java Kotlin泛型難點內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: