引子
《Effective Java》中第25條中《列表優于數組》中提到數組是協變的,相反泛型是不可變的
其實用于描述Java類型轉換后的繼承關系一共有三種,協變
,逆變
,不可變
其定義為:
如果A、B表示類型 f(?) 表示類型轉換,≤ 表示繼承關系(比如,A≤B 表示A是由B派生出來的子類);
f(?) 是逆變(contravariant)的,當
A≤B
時有f(B)≤f(A)
成立f(?) 是協變(covariant)的,當
A≤B
時有f(A)≤f(B)
成立f(?) 是不變(invariant)的,當
A≤B
時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關系
協變
數組是協變的,那就意味著String是Object的子類,則String[] 是 Object[]的子類,但是會有一個問題:
Object[] objArray = new Integer[1];
objArray[0] = "a string";
這段代碼是合法的,但是在運行時就會因為類型不符報錯
不可變
泛型是不可變的,這意味著
ArrayList<Object> objArray = new ArrayList<Object>();
objArray.add("a string");
是無法通過編譯的。根據不可變的定義,ArrayList<Object> 和 ArrayList<String>沒有繼承關系
這樣的設計是為了保證類型安全,根據《Effective Java》中的說法:
// Why generic array creation is illegal - won't compile
List<String>[] stringLists = new ArrayList<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
假設(1)是編譯正確的,那么在(5)的時候就必然會出現類型不匹配,因為它嘗試把整型賦值給字符類型的
泛型類型中利用通配符(extends/super)來實現協變和逆變
List<? extends Fruit> 表明每個item是Fruit/Fruit的子類,這其實表明了泛型的上線,實現了協變
同樣,List<? super Fruit> 表明每個item都是Fruit/Furit的基類,這表明了泛型的下線,實現了逆變
泛型的協變/逆變使用依靠著一個PECS原則,即Provider Extends Consumer Super
還以List為例:
// 前提為Apple為Fruit的派生子類
List<? extends Fruit> list = new ArrayList<Apple>();
list.add(new Apple());
這樣的寫法是無法通過編譯的,會提示類型不符,因為? extends Furit
表明了Furit或者Furit的派生子類
。如果我們存入Apple,在get時強轉為Apple自然是不會有問題,但是如果我存入Banana,同樣是水果,那么在get時就會報錯,這樣是類型不安全的
泛型通過擦除來實現的,
? extends Furit
在編譯階段只是一個標記,和數組具體化類型是不一樣的
那么作為一個Consumer,應當使用super
// 前提為Apple為Fruit的派生子類
List<? super Fruit> list = new ArrayList<Food>();
list.add(new Apple());
這段代碼是編譯通過的,只要類型要求是Fruit/Fruit的基類,那么存入的類型必定可以強轉為Fruit/Fruit的基類,是類型安全的