java 協變與逆變

我們首先來看下面這兩行代碼:

Integer a = 1;
Number number = a;

so easy對吧?那我們再看這兩行代碼:

List<Integer> list1 = new ArrayList<Integer>();
List<Number> list = list1;

你認爲第二行的list編譯器會通過麼?答案是不會的。要解答這個問題,我們就要聊到協變與逆變,泛型的通配符?和extends和super。
首先要想第二行的list編譯通過需要對其進行改造,改造如下:

List<? extends Number> list2 = list1;  
or
List<? super Integer> list3 =list1;

list2與list3有什麼區別呢?
list2只能獲取數據不能添加數據,list3只能添加數據不能獲取數據。
下面就有兩個問題:
1爲啥通過? extends或者? super就能解決list編譯不通過的問題。
2爲啥list2只能獲取數據,list3只能添加數據。
先回顧一下知識:我們知道,java中的泛型是不可變的,它會在JVM編譯時就進行類型擦除,當我們給你一個泛型屬性或者集合添加數據時都是需要明確指定具體類型的。通配符?是用來放任意類型的,比如List<? extends Number> list2,list2可以放入Integer類型的數據,也可以放入Float類型的數據。
對於第一個問題,因爲java泛型是不可變的,但是可以通過extends關鍵字可以提供協變的泛型類型轉換,通過supper可以提供逆變的泛型類型轉換。
對於第二個問題,我們跟源碼進去看看,源碼如下所示:

public interface List<E> extends Collection<E> {
	boolean add(E e);
}

我們可以看到List接口是泛型接口,泛型是不可變的,現在把List的泛型E變爲了Number,所以編譯器是不會通過的。同時我們也有個疑問,爲啥list2只能獲取數據,list3只能添加數據。
協變與逆變的定義:
假設有兩個類型A和B,則他們各自的構造類型分別爲f(A),f(B);
當A ≦ B時,如果有f(A) ≦ f(B),那麼f叫做協變;
當A ≦ B時,如果有f(B) ≦ f(A),那麼f叫做逆變;
我們也可以簡單理解所謂協變就是A是B的子類型,逆變就是A是B的父類型。我們可以看到協變會導致範圍縮小的,逆變會導致範圍擴大的。
針對list2,此時可以看到接口List的泛型E變成了? extends Number,這個? extends Number 通配符告訴編譯器我們在處理一個類型Number的子類型,但我們不知道這個子類型究竟是什麼。因爲沒法確定,爲了保證類型安全,java就不允許往裏面添加任何數據。針對list3,此時可以看到接口List的泛型E變成了? super Integer,其表示list所持有的類型爲Integer或者Integer的衆多父類之一的類型,如果從list3中獲取數據,我們就發現list3中的元素類型是不確定的,但是給list3添加元素都是確定的,都是Integer類型。從上面的例子可以看出,extends確定了泛型的上界,而super確定了泛型的下界。
擴展一下:

1jdk版本升級時引入協變。
在Java 1.4中,子類覆蓋(override)父類方法時,形參與返回值的類型必須與父類保持一致:

class Super {
    Number method(Number n) { ... }
}

class Sub extends Super {
    @Override 
    Number method(Number n) { ... }
}

從Java 1.5開始,子類覆蓋父類方法時允許協變返回更爲具體的類型:

class Super {
    Number method(Number n) { ... }
}
class Sub extends Super {
    @Override 
    Integer method(Number n) { ... }
}

2設計原則之一:里氏替換原則。
設計模式中有六大設計原則。而里氏替換原則就是其中之一,感興趣的朋友可以自行學習。
爲啥要說到里氏替換原則呢?我們先看里氏替換原則最直白的定義:
只要有父類出現的地方,都可以用子類來替代。
上面我們操作泛型集合list時,可以看到存取的泛型類型分別是? extends Number和? super Integer,因爲使用<? extends Number>後,如果泛型參數作爲返回值,用T接收一定是安全的,也就是說使用這個函數的人可以知道你生產了什麼東西;而使用<? super Integer>後,如果泛型參數作爲入參,傳遞T及其子類一定是安全的,也就是說使用這個函數的人可以知道你需要什麼東西來進行消費。
java.util.Collections的copy方法(JDK1.7)完美地詮釋了PECS,源碼如下:

public static <T> 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());
        }
    }
}

里氏替換原則(PECS)總結:
要從泛型類取數據時,用extends;
要往泛型類寫數據時,用super;
既要取又要寫,就不用通配符(即extends與super都不用)
參考博客:
https://www.cnblogs.com/keyi/p/6068921.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章