標籤:variant ceo 定義變數 ++ 使用者 size 用法 ons lang
JAVA中的協變與逆變
首先說一下關於Java中協變,逆變與不變的概念
比較官方的說法是逆變與協變描述的是類型轉換後的繼承關係。
定義A,B兩個類型,A是由B派生出來的子類(A<=B),f()表示類型轉換如new List();
協變: 當A<=B時,f(A)<=f(B)成立
逆變: 當A<=B時,f(B)<=f(A)成立
不變: 當A<=B時,上面兩個式子都不成立
這麼說可能理解上有些費勁,我們用代碼來表示一下協變和逆變
class Fruit {}class Apple extends Fruit {}class Jonathan extends Apple {}class Orange extends Fruit {}@Testpublic void testArray() { Fruit[] fruit = new Apple[10]; fruit[0] = new Apple(); fruit[1] = new Jonathan(); try { fruit[0] = new Fruit(); } catch (Exception e) { System.out.println(e); } try { fruit[0] = new Orange(); } catch (Exception e) { System.out.println(e); }}
Java中數組是協變的,可以向子類型的數組賦基底類型的數組引用。
Apple是Fruit的子類型,所以Apple的對象可以賦給Fruit對象。Apple<=Fruit Fruit的數群組類型是Fruit[],這個就是由Fruit物件建構出來的新的類型,即f(Fruit),同理,Apple[]就是Apple構造出來的新的類型,就是f(Apple)
所以上方代碼中的Fruit[] fruit = new Apple[10]是成立的,這也是物件導向編程中經常說的
子類變數能賦給父類變數,父類變數不能賦值給子類變數。
上方代碼中的try..catch中的在編譯器中是不會報錯的,但是在啟動並執行時候會報錯,因為在編譯器中數組的符號是Fruit類型,所以可以存放Fruit和Orange類型,但是在啟動並執行時候會發現實際類型是Apple[]類型,所以會報錯
java.lang.ArrayStoreException: contravariant.TestContravariant$Fruit
java.lang.ArrayStoreException: contravariant.TestContravariant$Orange
不變
@Testpublic void testList() { List<Fruit> fruitList = new ArrayList<Apple>();}
這樣的代碼在編譯器上會直接報錯。和數組不同,泛型沒有內建的協變類型,使用泛型的時候,類型資訊在編譯期會被類型擦除,所以泛型將這種錯誤偵測移到了編譯器。所以泛型是 不變的
泛型的協變
但是這樣就會出現一些很彆扭的情況,打個比方就是一個可以放水果的盤子裡面不能放蘋果。
所以為瞭解決這種問題,Java在泛型中引入了萬用字元,使得泛型具有協變和逆變的性質, 協變泛型的用法就是<? extends Fruit>
@Testpublic void testList() { List<? extends Fruit> fruitList = new ArrayList<Apple>(); // 編譯錯誤 fruitList.add(new Apple()); // 編譯錯誤 fruitList.add(new Jonathan()); // 編譯錯誤 fruitList.add(new Fruit()); // 編譯錯誤 fruitList.add(new Object());}
當使用了泛型的萬用字元之後,確實可以實現將ArrayList
因為,在定義了fruitList之後,編譯器只知道容器中的類型是Fruit或者它的子類,但是具體什麼類型卻不知道,編譯器不知道能不能比配上就都不允許比配了。類比數組,在編譯器的時候數組允許向數組中放Fruit和Orange等非法類型,但是運行時還是會報錯,泛型是將這種檢查移到了編譯期,協變的過程中丟失了類型資訊。
所以對於萬用字元,T和?的區別在於,T是一個具體的類型,但是?編譯器並不知道是什麼類型。不過這種用法並不影響從容器中取值。
List<? extends Fruit> fruitList = new ArrayList
Fruit fruit = fruitList.get(0);
Object object = fruitList.get(0);
// 編譯錯誤
Apple apple = fruitList.get(0);
泛型的逆變
@Test
public void testList() {
List<? super Apple> appleList = new ArrayList
Object object = appleList.get(0); appleList.add(new Apple()); appleList.add(new Jonathan()); // 編譯錯誤 appleList.add(new Fruit()); // 編譯錯誤 appleList.add(new Object());}
可以看到使用super就可以實現泛型的逆變,使用super的時候指出了泛型的下界是Apple,可以接受Apple的父類型,既然是Apple的父類型,編輯器就知道了向其中添加Apple或者Apple的子類是安全的了,所以,此時可以向容器中進行存,但是取的時候編輯器只知道是Apple的父類型,具體什麼類型還是不知道,所以只有取值會出現編譯錯誤,除非是取Object類型。
泛型協變逆變的用法
當平時定義變數的時候肯定不能像上面的例子一樣使用泛型的萬用字元,具體的泛型萬用字元的使用方法在Effective Jave一書的第28條中有總結:
為了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入參數上使用萬用字元類型。如果每個輸入參數既是生產者,又是消費者,那麼萬用字元類型對你就沒有什麼好處了:因為你需要的是嚴格的類型比配,這是不用任何萬用字元而得到的。
簡單來說就是PECS表示->producer-extends,consumer-super。
不要使用萬用字元類型作為傳回型別,除了為使用者提供額外的靈活性之外,它還會強制使用者在用戶端代碼中使用萬用字元類型。萬用字元類型對於類的使用者來說應該是無形的,它們使方法能夠接受它們應該接受的參數,並拒絕那些應該拒絕的參數,如果類的使用者必須考慮萬用字元類型,類的API或許就會出錯。
一個經典的例子就是java.uitl.Collections中的copy方法
public static
if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } }}
dest為生產者只從其中取資料,src為消費者,只存放資料進去。
JAVA中的協變與逆變