JAVA中的協變與逆變


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 {}

@Test
public 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
不變

@Test
public void testList() {
    List<Fruit> fruitList = new ArrayList<Apple>();
}

這樣的代碼在編譯器上會直接報錯。和數組不同,泛型沒有內建的協變類型,使用泛型的時候,類型信息在編譯期會被類型擦除,所以泛型將這種錯誤檢測移到了編譯器。所以泛型是 不變的

泛型的協變

但是這樣就會出現一些很別扭的情況,打個比方就是一個可以放水果的盤子里面不能放蘋果。

所以為了解決這種問題,Java在泛型中引入了通配符,使得泛型具有協變和逆變的性質, 協變泛型的用法就是<? extends Fruit>

@Test
public 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 進行向上轉型了,實現了泛型的協變,但是卻再也不能往容器中放任何東西了,連Apple本身都被禁止了

因為,在定義了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 ();
// 編譯錯誤
Fruit fruit = appleList.get(0);
// 編譯錯誤
Apple apple = appleList.get(0);
// 編譯錯誤
Jonathan jonathan = appleList.get(0);

    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 void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

    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為消費者,只存放數據進去。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM