欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一篇文章弄懂Java和Kotlin的泛型難點(diǎn)

 更新時(shí)間:2021年05月12日 11:42:34   作者:安卓老猴子  
這篇文章主要給大家介紹了如何通過一篇文章弄懂Java和Kotlin的泛型難點(diǎn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

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

一、泛型類型

泛型允許你定義帶類型形參的數(shù)據(jù)類型,當(dāng)這種類型的實(shí)例被創(chuàng)建出來后,類型形參便被替換為稱為類型實(shí)參的具體類型。例如,對于 List<T>,List 稱為基礎(chǔ)類型,T 便是類型型參,T 可以是任意類型,當(dāng)沒有指定 T 的具體類型時(shí),我們只能知道List<T>是一個集合列表,但不知道承載的具體數(shù)據(jù)類型。而對于 List<String>,當(dāng)中的 String 便是類型實(shí)參,我們可以明白地知道該列表承載的都是字符串,在這里 String 就相當(dāng)于一個參數(shù)傳遞給了 List,在這語義下 String 也稱為類型參數(shù)

此外,在 Kotlin 中我們可以實(shí)現(xiàn)實(shí)化類型參數(shù),在運(yùn)行時(shí)的內(nèi)聯(lián)函數(shù)中拿到作為類型實(shí)參的具體類型,即可以實(shí)現(xiàn) T::class.java,但在 Java 中卻無法實(shí)現(xiàn),因?yàn)閮?nèi)聯(lián)函數(shù)是 Kotlin 中的概念,Java 中并不存在

二、為什么需要泛型

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

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

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

而有了泛型后,我們就可以寫出更加健壯安全的代碼,以下錯誤就完全可以在編譯階段被發(fā)現(xiàn),且取值的時(shí)候也不需要進(jìn)行類型強(qiáng)轉(zhuǎn)

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

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

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

    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;
    }

總的來說,泛型有以下幾點(diǎn)優(yōu)勢:

  • 類型檢查,在編譯階段就能發(fā)現(xiàn)錯誤
  • 更加語義化,看到 List<String>我們就知道存儲的數(shù)據(jù)類型是 String
  • 自動類型轉(zhuǎn)換,在取值時(shí)無需進(jìn)行手動類型轉(zhuǎn)換
  • 能夠?qū)⑦壿嫵橄蟪鰜?,使得代碼更加具有通用性

三、類型擦除

泛型是在 Java 5 版本開始引入的,所以在 Java 4 中 ArrayList 還不屬于泛型類,其內(nèi)部通過 Object 向上轉(zhuǎn)型和外部強(qiáng)制類型轉(zhuǎn)換來實(shí)現(xiàn)數(shù)據(jù)存儲和邏輯復(fù)用,此時(shí)開發(fā)者的項(xiàng)目中已經(jīng)充斥了大量以下類型的代碼:

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

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

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

為了實(shí)現(xiàn)這一目的,Java 就通過類型擦除這種比較別扭的方式來實(shí)現(xiàn)泛型。編譯器在編譯時(shí)會擦除類型實(shí)參,在運(yùn)行時(shí)不存在任何類型相關(guān)的信息,泛型對于 JVM 來說是透明的,有泛型和沒有泛型的代碼通過編譯器編譯后所生成的二進(jìn)制代碼是完全相同的

例如,分別聲明兩個泛型類和非泛型類,拿到其 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("業(yè)志陳");
        NodeB<String> nodeB = new NodeB<>("業(yè)志陳");
        System.out.println(nodeB.obj);
    }

}

可以看到 NodeA 和 NodeB 兩個對象對應(yīng)的字節(jié)碼其實(shí)是完全一樣的,最終都是使用 Object 來承載數(shù)據(jù),就好像傳遞給 NodeB 的類型參數(shù) 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 業(yè)志陳
       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 業(yè)志陳
      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 設(shè)定上界約束 String,兩者的字節(jié)碼也會完全一樣

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("業(yè)志陳");
        NodeB<String> nodeB = new NodeB<>("業(yè)志陳");
        System.out.println(nodeB.obj);
    }

}

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

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 業(yè)志陳
       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 業(yè)志陳
      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
}

所以說,當(dāng)泛型類型被擦除后有兩種轉(zhuǎn)換方式

  • 如果泛型沒有設(shè)置上界約束,那么將泛型轉(zhuǎn)化成 Object 類型
  • 如果泛型設(shè)置了上界約束,那么將泛型轉(zhuǎn)化成該上界約束

該結(jié)論也可以通過反射泛型類的 Class 對象來驗(yàn)證

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<>("業(yè)志陳");
        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 對應(yīng)的是 Object,NodeB 對應(yīng)的是 String

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

那既然在運(yùn)行時(shí)不存在任何類型相關(guān)的信息,泛型又為什么能夠?qū)崿F(xiàn)類型檢查和類型自動轉(zhuǎn)換等功能呢?

其實(shí),類型檢查是編譯器在編譯前幫我們完成的,編譯器知道我們聲明的具體的類型實(shí)參,所以類型擦除并不影響類型檢查功能。而類型自動轉(zhuǎn)換其實(shí)是通過內(nèi)部強(qiáng)制類型轉(zhuǎn)換來實(shí)現(xiàn)的,上面給出的字節(jié)碼中也可以看到有一條類型強(qiáng)轉(zhuǎn) checkcast 的語句

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

例如,ArrayList 內(nèi)部雖然用于存儲數(shù)據(jù)的是 Object 數(shù)組,但 get 方法內(nèi)部會自動完成類型強(qiáng)轉(zhuǎn)

transient Object[] elementData;

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

@SuppressWarnings("unchecked")
E elementData(int index) {
 //強(qiáng)制類型轉(zhuǎn)換
 return (E) elementData[index];
}

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

四、類型擦除的后遺癥

Java 泛型對于類型的約束只在編譯期存在,運(yùn)行時(shí)仍然會按照 Java 5 之前的機(jī)制來運(yùn)行,泛型的具體類型在運(yùn)行時(shí)已經(jīng)被刪除了,所以 JVM 是識別不到我們在代碼中指定的具體的泛型類型的

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

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

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

}

我們只能對具體的對象實(shí)例進(jìn)行類型校驗(yàn),但無法判斷出泛型形參的具體類型

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

 }
 //報(bào)錯
 if (T instanceof String) {

 }
 //報(bào)錯
 Class<T> tClass = T::getClass;
}

此外,類型擦除也會導(dǎo)致 Java 中出現(xiàn)多態(tài)問題。例如,以下兩個方法的方法簽名并不完全相同,但由于類型擦除的原因,入?yún)?shù)的數(shù)據(jù)類型都會被看成 List<Object>,從而導(dǎo)致兩者無法共存在同一個區(qū)域內(nèi)

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())
    //泛型類型自動推導(dǎo)
    val plateBanana = Plate(Banana())
    plateApple.cut()
    plateBanana.cut()
}

Kotlin 也支持在擴(kuò)展函數(shù)中使用泛型

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

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

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

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

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

還有一個比較容易讓人誤解的點(diǎn)。我們經(jīng)常會使用 as 和 as? 來進(jìn)行類型轉(zhuǎn)換,但如果轉(zhuǎn)換對象是泛型類型的話,那就會由于類型擦除而出現(xiàn)誤判。如果轉(zhuǎn)換對象有正確的基礎(chǔ)類型,那么轉(zhuǎn)換就會成功,而不管類型實(shí)參是否相符。因?yàn)樵谶\(yùn)行時(shí)轉(zhuǎn)換發(fā)生的時(shí)候類型實(shí)參是未知的,此時(shí)編譯器只會發(fā)出 “unchecked cast” 警告,代碼還是可以正常編譯的

例如,在以下例子中代碼的運(yùn)行結(jié)果還符合我們的預(yù)知。第一個轉(zhuǎn)換操作由于類型相符,所以打印出了相加值。第二個轉(zhuǎn)換操作由于基礎(chǔ)類型是 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,這是因?yàn)樵谶\(yùn)行時(shí)不會判斷且無法判斷出類型實(shí)參到底是否是 Int,而只會判斷基礎(chǔ)類型 List 是否相符,所以 as? 操作會成功,等到要執(zhí)行相加操作時(shí)才會發(fā)現(xiàn)拿到的是 String 而非 Number

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

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

六、上界約束

泛型本身已經(jīng)帶有類型約束的作用,我們也可以進(jìn)一步細(xì)化其支持的具體類型

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

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()) //報(bào)錯
}

如果上界約束擁有多層類型元素,Java 是使用 & 符號進(jìn)行鏈?zhǔn)铰暶鳎琄otlin 則是用 where 關(guān)鍵字來依次進(jìn)行聲明

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()) //報(bào)錯
    val bananaPlate = Plate(Banana()) //正常
}

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

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

假設(shè)現(xiàn)在有個需求,需要我們提供一個方法用于遍歷所有類型的 List 集合并打印元素

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

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

可能會想到的第二種做法是:將泛型類型直接聲明為 Object,希望讓其適用于任何類型的 List。這種做法完全不可行,因?yàn)榧词?String 是 Object 的子類,但 List<String> 和 List<Object>并不具備從屬關(guān)系,這導(dǎo)致 printList2 方法實(shí)際上只能用于List<Object>這一種具體類型

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

最優(yōu)解法就是要用到 Java 的類型通配符 ? 了,printList3方法完全可行且編譯器也不會警告報(bào)錯

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

? 表示我們并不關(guān)心具體的泛型類型,而只是想配合其它類型進(jìn)行一些條件限制。例如,printList3方法希望傳入的是一個 List,但不限制泛型的具體類型,此時(shí)List<?>就達(dá)到了這一層限制條件

類型通配符也存在著一些限制。因?yàn)?printList3 方法并不包含具體的泛型類型,所以我們從中取出的值只能是 Object 類型,且無法向其插入值,這都是為了避免發(fā)生 ClassCastException

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

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

此外,星號投影只能出現(xiàn)在類型形參的位置,不能作為類型實(shí)參

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

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

八、協(xié)變 & 不變

看以下例子。Apple 和 Banana 都是 Fruit 的子類,可以發(fā)現(xiàn) Apple[] 類型的對象是可以賦值給 Fruit[] 的,且 Fruit[] 可以容納 Apple 對象和 Banana 對象,這種設(shè)計(jì)就被稱為協(xié)變,即如果 A 是 B 的子類,那么 A[] 就是 B[] 的子類型。相對的,Object[] 就是所有數(shù)組對象的父類型

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();
    //編譯時(shí)正常,運(yùn)行時(shí)拋出 ArrayStoreException
    fruitArray[1] = new Banana();
}

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

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

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

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

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

再來想個問題,既然協(xié)變本身并不安全,那么數(shù)組為何又要被設(shè)計(jì)為協(xié)變呢?

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

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 中的數(shù)組和 Java 中的數(shù)組并不一樣,Kotlin 數(shù)組并不支持協(xié)變,Kotlin 數(shù)組類似于集合框架,具有對應(yīng)的實(shí)現(xiàn)類 Array,Array 屬于泛型類,支持了泛型因此也不再協(xié)變

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

Java 的泛型也并非完全不變的,只是實(shí)現(xiàn)協(xié)變需要滿足一些條件,甚至也可以實(shí)現(xiàn)逆變,下面就來介紹下泛型如何實(shí)現(xiàn)協(xié)變和逆變

九、泛型協(xié)變

假設(shè)我們定義了一個copyAll希望用于 List 數(shù)據(jù)遷移。那以下操作在我們看來就是完全安全的,因?yàn)?Integer 是 Number 的子類,按道理來說是能夠?qū)?Integer 保存為 Number 的,但由于泛型不變性,List<Integer>并不是List<Number>的子類型,所以實(shí)際上該操作將報(bào)錯

    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); //報(bào)錯
    }

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

思考下該操作為什么會報(bào)錯?

編譯器的作用之一就是進(jìn)行安全檢查并阻止可能發(fā)生不安全行為的操作,copyAll 方法會報(bào)錯,那么肯定就是編譯器覺得該方法有可能會觸發(fā)不安全的操作。開發(fā)者的本意是希望將 Integer 類型的數(shù)據(jù)轉(zhuǎn)移到 NumberList 中,只有這種操作且這種操作在我們看來肯定是安全的,但是編譯器不知道開發(fā)者最終所要做的具體操作啊

假設(shè) copyAll方法可以正常調(diào)用,那么copyAll方法自然只會把 from 當(dāng)做 List<Number>來看待。因?yàn)?Integer 是 Number 的子類,從 integerList 獲取到的數(shù)據(jù)對于 numberList 來說自然是安全的。而如果我們在copyAll方法中偷偷向 integerList 傳入了一個 Number 類型的值的話,那么自然就將拋出異常,因?yàn)?from 實(shí)際上是 List<Integer>類型

為了阻止這種不安全的行為,編譯器選擇通過直接報(bào)錯來進(jìn)行提示。為了解決報(bào)錯,我們就需要向編譯器做出安全保證:從 from 取出來的值只會當(dāng)做 Number 類型,且不會向 from 傳入任何值

為了達(dá)成以上保證,需要修改下 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 執(zhí)行傳值操作。有了該限制后,從integerList中取出來的值只能是當(dāng)做 Number 類型,且避免了向integerList插入非法值的可能,此時(shí)List<Integer>就相當(dāng)于List<? extends Number>的子類型了,從而使得 copyAll 方法可以正常使用

簡而言之,帶 extends 限定了上界的通配符類型使得泛型參數(shù)類型是協(xié)變的,即如果 A 是 B 的子類,那么 Generic<A> 就是Generic<? extends B>的子類型

十、泛型逆變

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

協(xié)變還比較好理解,畢竟其繼承關(guān)系是相同的,但逆變就比較反直覺了,整個繼承關(guān)系都倒過來了

逆變的作用可以通過相同的例子來理解,copyAll 方法如下修改也可以正常使用,此時(shí)就是向編譯器做出了另一種安全保證:向 numberList 傳遞的值只會是 Integer 類型,且從 numberList 取出的值也只會當(dāng)做 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 中獲取到非法類型值的可能,此時(shí)List<Number>就相當(dāng)于List<? super Integer>的子類型了,從而使得 copyAll 方法可以正常使用

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

十一、out & in

Java 中關(guān)于泛型的困境在 Kotlin 中一樣存在,out 和 in 都是 Kotlin 的關(guān)鍵字,其作用都是為了來應(yīng)對泛型問題。in 和 out 是一個對立面,同時(shí)它們又與泛型不變相對立,統(tǒng)稱為型變

  • out 本身帶有出去的意思,本身帶有傾向于取值操作的意思,用于泛型協(xié)變
  • in 本身帶有進(jìn)來的意思,本身帶有傾向于傳值操作的意思,用于泛型逆變

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

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

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

    copyAll(numberList, intList) //報(bào)錯

    numberList.forEach {
        println(it)
    }
}

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

報(bào)錯原因和 Java 完全一樣,因?yàn)榇藭r(shí)編譯器無法判斷出我們到底是否會做出不安全的操作,所以我們依然要來向編譯器做出安全保證

此時(shí)就需要在 Kotlin 中來實(shí)現(xiàn)泛型協(xié)變和泛型逆變了,以下兩種方式都可以實(shí)現(xiàn):

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 關(guān)鍵字就相當(dāng)于 Java 中的<? extends T>,其作用就是限制了 from 不能用于接收值而只能向其取值,這樣就避免了從 to 取出值然后向 from 賦值這種不安全的行為了,即實(shí)現(xiàn)了泛型協(xié)變

in 關(guān)鍵字就相當(dāng)于 Java 中的<? super T>,其作用就是限制了 to 只能用于接收值而不能向其取值,這樣就避免了從 to 取出值然后向 from 賦值這種不安全的行為了,即實(shí)現(xiàn)了泛型逆變

從這也可以聯(lián)想到,MutableList<*> 就相當(dāng)于 MutableList<out Any?>了,兩者都帶有相同的限制條件:不允許寫值操作,允許讀值操作,且讀取出來的值只能當(dāng)做 Any?進(jìn)行處理

十二、支持協(xié)變的 List

在上述例子中,想要實(shí)現(xiàn)協(xié)變還有另外一種方式,那就是使用 List

將 from 的類型聲明從 MutableList<T>修改為 List<T> 后,可以發(fā)現(xiàn) copyAll 方法也可以正常調(diào)用了

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

對 Kotlin 有一定了解的同學(xué)應(yīng)該知道,Kotlin 中的集合框架分為兩種大類:可讀可寫和只能讀不能寫

以 Java 中的 ArrayList 為例,Kotlin 將之分為了 MutableList 和 List 兩種類型的接口。而 List 接口中的泛型已經(jīng)使用 out 關(guān)鍵字進(jìn)行修飾了,且不包含任何傳入值并保存的方法,即 List 接口只支持讀值而不支持寫值,其本身就已經(jīng)滿足了協(xié)變所需要的條件,因此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 類型的入?yún)?shù),但該方法本身不會進(jìn)行寫值操作,所以實(shí)際上可以正常使用,Kotlin 也使用 @UnsafeVariance抑制了編譯器警告

十三、reified & inline

上文講了,由于類型擦除,Java 和 Kotlin 的泛型類型實(shí)參都會在編譯階段被擦除,在 Kotlin 中存在一個額外手段可以來解決這個問題,即內(nèi)聯(lián)函數(shù)

用關(guān)鍵字 inline 標(biāo)記的函數(shù)就稱為內(nèi)聯(lián)函數(shù),再用 reified 關(guān)鍵字修飾內(nèi)聯(lián)函數(shù)中的泛型形參,編譯器在進(jìn)行編譯的時(shí)候便會將內(nèi)聯(lián)函數(shù)的字節(jié)碼插入到每一個調(diào)用的地方,當(dāng)中就包括泛型的類型實(shí)參。而內(nèi)聯(lián)函數(shù)的類型形參能夠被實(shí)化,就意味著我們可以在運(yùn)行時(shí)引用實(shí)際的類型實(shí)參了

例如,我們可以寫出以下這樣的一個內(nèi)聯(lián)函數(shù),用于判斷一個對象是否是指定類型

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

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

將以上的 Kotlin 代碼反編譯為 Java 代碼,可以看出來 main()方法最終是沒有調(diào)用 isInstanceOf 方法的,具體的判斷邏輯都被插入到了main()方法內(nèi)部,最終是執(zhí)行了 instanceof 操作,且指定了具體的泛型類型參數(shù) 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 反序列的時(shí)候。由于泛型運(yùn)行時(shí)類型擦除的問題,目前用 Gson 反序列化泛型類時(shí)步驟是比較繁瑣的,利用 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":"業(yè)志陳","url":"https://juejin.cn/user/923245496518439"}"""
    val listJson = """[{"name":"業(yè)志陳","url":"https://juejin.cn/user/923245496518439"},{"name":"業(yè)志陳","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=業(yè)志陳, url=https://juejin.cn/user/923245496518439)
    println(blogBean)
    //{name=業(yè)志陳, url=https://juejin.cn/user/923245496518439}
    println(blogMap)
    //[{name=業(yè)志陳, url=https://juejin.cn/user/923245496518439}, {name=業(yè)志陳, url=https://juejin.cn/user/923245496518439}]
    println(blogBeanList)
}

我也利用 Kotlin 的這個強(qiáng)大特性寫了一個用于簡化 Java / Kotlin 平臺的序列化和反序列化操作的庫:JsonHolder

十四、總結(jié)

最后來做個簡單的總結(jié)

協(xié)變 逆變 不變
Kotlin <out T>,只能作為消費(fèi)者,只能讀取不能添加 <in T>,只能作為生產(chǎn)者,只能添加,讀取出的值只能當(dāng)做 Any 類型 <T>,既可以添加也可以讀取
Java <? extends T>,只能作為消費(fèi)者,只能讀取不能添加 <? super T>,只能作為生產(chǎn)者,只能添加,讀取出的值只能當(dāng)做 Object 類型 <T>,既可以添加也可以讀取

到此這篇關(guān)于Java和Kotlin的泛型難點(diǎn)的文章就介紹到這了,更多相關(guān)Java Kotlin泛型難點(diǎn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 一文搞懂java中類及static關(guān)鍵字執(zhí)行順序

    一文搞懂java中類及static關(guān)鍵字執(zhí)行順序

    這篇文章主要介紹了一文搞懂java中類及static關(guān)鍵字執(zhí)行順序,文章通過類的生命周期展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • 通過實(shí)例解析Spring組合注解與元注解

    通過實(shí)例解析Spring組合注解與元注解

    這篇文章主要介紹了通過實(shí)例解析Spring組合注解與元注解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-11-11
  • SpringBoot登錄用戶權(quán)限攔截器

    SpringBoot登錄用戶權(quán)限攔截器

    這篇文章主要介紹了SpringBoot登錄用戶權(quán)限攔截器,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • java中的char占幾個字節(jié)實(shí)例分析

    java中的char占幾個字節(jié)實(shí)例分析

    這篇文章主要介紹了java中的char占幾個字節(jié)實(shí)例分析的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • Java線程生命周期圖文詳細(xì)講解

    Java線程生命周期圖文詳細(xì)講解

    在java中,任何對象都要有生命周期,線程也不例外,它也有自己的生命周期。線程的整個生命周期可以分為5個階段,分別是新建狀態(tài)、就緒狀態(tài)、運(yùn)行狀態(tài)、阻塞狀態(tài)和死亡狀態(tài)
    2023-01-01
  • java基礎(chǔ)之Integer與int類型輸出示例解析

    java基礎(chǔ)之Integer與int類型輸出示例解析

    這篇文章主要為大家介紹了java基礎(chǔ)之Integer與int類型輸出示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • Java 中函數(shù) Function 的使用和定義示例小結(jié)

    Java 中函數(shù) Function 的使用和定義示例小結(jié)

    這篇文章主要介紹了Java 中函數(shù) Function 的使用和定義小結(jié),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2024-07-07
  • Hadoop集成Spring的使用詳細(xì)教程(快速入門大數(shù)據(jù))

    Hadoop集成Spring的使用詳細(xì)教程(快速入門大數(shù)據(jù))

    這篇文章主要介紹了Hadoop集成Spring的使用詳細(xì)教程(快速入門大數(shù)據(jù)),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-01-01
  • 一篇文章帶你了解Java容器,面板及四大布局管理器應(yīng)用

    一篇文章帶你了解Java容器,面板及四大布局管理器應(yīng)用

    這篇文章主要介紹了JAVA布局管理器與面板組合代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2021-08-08
  • springboot整合redis集群過程解析

    springboot整合redis集群過程解析

    這篇文章主要介紹了springboot整合redis集群過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-09-09

最新評論