圖解java泛型的協變和逆變

參考文獻:https://www.jianshu.com/p/2bf15c5265c5

https://www.jianshu.com/p/da1127c51c90


今天剛開始看kotlin的泛型語法和概念,覺得之前java中學過泛型,可能這個也差不多吧。。。。。嗯,確實差不多,想着跟之前一樣用類比java的方式繼續理解kotlin泛型,結果看了兩篇java的泛型之後。。。。。。發現java泛型之前沒怎麼學懂

之前在學java泛型時候沒有接觸到的兩個概念:協變和逆變。下面提到的可能大家都知道,只是我已自己的理解將協變和逆變的概念表述出來:


一、協變逆變概念

逆變與協變用來描述類型轉換(type transformation)後的繼承關係:A、B表示類型,f(·)表示類型轉換,A<=B表示A爲B的子類,那麼則存在:

  • f(·)是協變的:當A<=B   ,f(A)<=f(B)成立
  • f(·)是逆變的:當A<=B   ,f(A)>=f(B)成立
  • f(·)是不變的:當A<=B   ,f(A) 和f(B)不存在繼承關係

看的有點懵逼?先彆着急,等會兒回過頭來再看這個。。這裏介紹了協變和逆變的概念,對於java中數組是協變的,如下所示:

    public static void main(String[] args) {
        String[] strings = new String[5];
        Object[] objects = strings;
    }

實例中創建了一個字符串數組對象,但是用Object數組同樣可以引用。

實例中String類<=Object類,對應的String[]<=Object[],所以可以得出數組是協變類型。

現在問題來了:

現在將string類型的數組引用賦值給了object類型的數組引用,在操作時候是不是可以賦值除了string意外的類型呢?

    public static void main(String[] args) {
        String[] strings = new String[5];
        Object[] objects = strings;
        try {
            objects[0] = 1;
            System.out.println(strings[0]);
        } catch (Exception e) {
            System.out.println("出錯了吧。。。。。");
            e.printStackTrace();
        }
    }

給objects第一個元素設置一個int類型的1,結果如下:

java.lang.ArrayStoreException: java.lang.Integer
出錯了吧。。。。。
	at as.a.Str.main(Str.java:12)

看來數組以協變方式允許類型向上轉型,但是會有寫入安全的問題,如上異常

 

現在我們看下在集合中使用會是怎麼樣的:

    public static void main(String[] args) {
        String[] strings = new String[5];
        Object[] objects = strings;
        try {
            objects[0] = 1;
            System.out.println(strings[0]);
        } catch (Exception e) {
            System.out.println("出錯了吧。。。。。");
            e.printStackTrace();
        }


        List<String> strList = new ArrayList<>();
        List<Object> objList = strList;//編譯錯誤了
    }

在將strList賦值給objList時候,已經出現編譯錯誤了,錯誤結果如下:

使用泛型時,在編譯期間存在泛型擦除過程,取消了運行時檢查,所以將泛型的錯誤檢查提前到了編譯器。並不是因爲是兩個不同的類型。

這時候用到了泛型通配符? extends T  和 ? super T了。。首先示例的繼承關係如下:

 


class 生物 {
}

class 動物 extends 生物 {
}

class 人 extends 動物 {
}

class 狗 extends 動物 {
}

class 山頂洞人 extends 人 {
}

class 半坡人 extends 人 {

}

示例如下:

public class Str {
    public static void main(String[] args) {

        List<? extends 動物> objList = new ArrayList<人>();
        動物 動物 = objList.get(0);//編譯通過
        生物 動物1 = objList.get(0);//編譯通過
        人 人 = objList.get(0);//編譯錯誤
        objList.add(new 動物());//編譯錯誤
        objList.add(new 人());//編譯錯誤
        objList.add(new 狗());//編譯錯誤
    }

}

示例中將動物和生物類型引用objList的元素時,編譯無錯誤,但是將人類型引用objList元素時,編譯出錯了。然後,,,,,,不管什麼類型,只要add就全都編譯錯誤了。

我是這樣想的,如果說他允許add T及其子類對象,那他是如何知道哪些類型的對象是應該添加的呢?舉個簡單的🌰,List<? extends 動物> 存放都是動物的子類,但是無法確定是哪一個子類,這種情況下依然會出現安全問題(如上慄中String數組中+int);而接收引用也同樣是這個道理:我存放的是你T的子類,但是無不知道啊,那我接收的引用只要是你的父類就好啦。

這裏簡單總結一下上限通配符? extends T 的用法,? extends T表示所存儲類型都是T及其子類,但是獲取元素所使用的引用類型只能是T或者其父類。使用上限通配符實現向上轉型,但是會失去存儲對象的能力。上限通配符爲集合的協變表示

想要存儲對象,就需要下限通配符 ?super T 了,用法如下:

    public static void main(String[] args) {

        List<? super 人> humList = new ArrayList<>();
        humList.add(new 半坡人());//編譯通過
        humList.add(new 山頂洞人());//編譯通過
        humList.add(new 人());//編譯通過
        humList.add(new 動物());//編譯失敗

    }

相信大家一眼就看出來了,添加人及其子類沒有錯誤,一旦再網上就出現編譯錯誤了。

下限通配符 ? super T表示 所存儲類型爲T及其父類,但是添加的元素類型只能爲T及其子類,而獲取元素所使用的類型只能是Object,因爲Object爲所有類的基類。下限通配符爲集合的逆變表示。

現在反過頭來看一下最開始說的協變和逆變的概念:

  • 當使用上限通配符時,類的等級越高,所包含的範圍越大,符合協變的概念。
  • 當使用下限通配符時,類的等級越高,所包含的範圍越小,符合逆變的概念。

以下是筆者對以上內容的總結四句話:

?extends T 存放的類型一定爲T及其子類,但是獲取要用T或者其父類引用。轉型一致性

 

?super T 存放的類型一定爲T的父類,但添加一定爲T和其子類對象。轉型一致性

 

?extends T 進行add(T子類)編譯出錯:因爲無法確定到底是哪個子類

 

?super T get()對象,都是Object類型,因爲T的最上層父類是Object,想要向下轉型只能強轉。

 

對於泛型還有生產者消費者的概念,筆者打算放在下一篇和kotlin的泛型卸寫在一起。

 

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