1. 程式人生 > 其它 >圖解java泛型的協變和逆變

圖解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的泛型卸寫在一起。