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