天天看點

泛型的協變與逆變協變與逆變自限定的類型捕獲轉換

看下面一段代碼:

Number num = new Integer(1);  
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch

List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
list.add(new Float(1.2f));  //error
           

Integer是Number的子類,Integer類型的執行個體可以指派給Number類型的變量,為什麼ArrayList<Integer>不可以指派給ArrayList<Number>?這需要我們了解Java中的泛型通配符以及協變與逆變。

協變與逆變

Liskov替換原則

所有引用基類(父類)的地方必須能透明地使用其子類的對象。

LSP包含以下四層含義:

  • 子類完全擁有父類的方法,且具體子類必須實作父類的抽象方法。
  • 子類中可以增加自己的方法。
  • 當子類覆寫或實作父類的方法時,方法的形參要比父類方法的更為寬松。
  • 當子類覆寫或實作父類的方法時,方法的傳回值要比父類更嚴格。

定義

逆變與協變用來描述類型轉換(type transformation)後的繼承關系,其定義:如果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)互相之間沒有繼承關系。

類型協變性

數組是協變的

// CovariantArrays.java
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
    public static void main(String[] args) {
        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);
        }
    }
}
           

fruit數組在編譯期間是可以編譯的。但是在運作期間會出異常。因為fruit[0]是Apple類型的,在指派為Orange類型時出異常。

泛型是不變的

方法

調用方法

result = method(n)

;根據Liskov替換原則,傳入形參n的類型應為method形參的子類型,即

typeof(n)≤typeof(method's parameter)

;result應為method傳回值的基類型,即

typeof(methods's return)≤typeof(result)

static Number method(Number num) {

    return 1;

}

Object result = method(new Integer(2)); //correct

Number result = method(new Object()); //error

Integer result = method(new Integer(2)); //error

在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) { ... }

}

通配符引入協變、逆變

Java中泛型是不變的,可有時需要實作逆變與協變,怎麼辦呢?這時,通配符?派上了用場:

<? extends>實作了泛型的協變,比如:

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

<? super>實作了泛型的逆變,比如:

List<? super Number> list = new ArrayList<Object>();

extends與super

為什麼(開篇代碼中)List<? extends Number> list在add Integer和Float會發生編譯錯誤?首先,我們看看add的實作:

public interface List<E> extends Collection<E> {

    boolean add(E e);

}

在調用add方法時,泛型E自動變成了<? extends Number>,其表示list所持有的類型為在Number與Number派生子類中的某一類型,其中包含Integer類型卻又不特指為Integer類型(Integer像個備胎一樣!!!),故add Integer時發生編譯錯誤。為了能調用add方法,可以用super關鍵字實作:

List<? super Number> list = new ArrayList<Object>();

list.add(new Integer(1));

list.add(new Float(1.2f));

<? super Number>表示list所持有的類型為在Number與Number的基類中的某一類型,其中Integer與Float必定為這某一類型的子類;是以add方法能被正确調用。從上面的例子可以看出,extends确定了泛型的上界,而super确定了泛型的下界。

PECS

現在問題來了:究竟什麼時候用extends什麼時候用super呢?《Effective Java》給出了答案:

PECS: producer-extends, consumer-super.

如果類型形參表示一個T生産者,就使用<? extends T>,如果表示一個消費者,就使用<? super T>。

比如,一個簡單的Stack API:

public class  Stack<E>{
    public Stack();
    public void push(E e):
    public E pop();
    public boolean isEmpty();
}
           

要實作

pushAll(Iterable<E> src)

方法,将src的元素逐一入棧:

public void pushAll(Iterable<E> src){
    for(E e : src)
        push(e)
}
           

假設有一個執行個體化

Stack<Number>

的對象stack,src有

Iterable<Integer>

與 

Iterable<Float>

;在調用pushAll方法時會發生type mismatch錯誤,因為Java中泛型是不可變的,

Iterable<Integer>

與 

Iterable<Float>

都不是

Iterable<Number>

的子類型。是以,應改為

// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
           

要實作

popAll(Collection<E> dst)

方法,将Stack中的元素依次取出add到dst中,如果不用通配符實作:

// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());   
}
           

同樣地,假設有一個執行個體化

Stack<Number>

的對象stack,dst為

Collection<Object>

;調用popAll方法是會發生type mismatch錯誤,因為

Collection<Object>

不是

Collection<Number>

的子類型。因而,應改為:

// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
        dst.add(pop());
}
           

在上述例子中,在調用pushAll方法時生産了E 執行個體(produces E instances),在調用popAll方法時dst消費了E 執行個體(consumes E instances)。Naftalin與Wadler将PECS稱為Get and Put Principle。

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都不用)。

自限定的類型

了解自限定

Java泛型中,有一個好像是經常性出現的慣用法,它相當令人費解。

class SelfBounded<T extends SelfBounded<T>> { // ...
           

SelfBounded類接受泛型參數T,而T由一個邊界類限定,這個邊界就是擁有T作為其參數的SelfBounded,看起來是一種無限循環。

先給出結論:這種文法定義了一個基類,這個基類能夠使用子類作為其參數、傳回類型、作用域。為了了解這個含義,我們從一個簡單的版本入手。

// BasicHolder.java
public class BasicHolder<T> {
    T element;
    void set(T arg) { element = arg; }
    T get() { return element; }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

// CRGWithBasicHolder.java
class Subtype extends BasicHolder<Subtype> {}

public class CRGWithBasicHolder {
    public static void main(String[] args) {
        Subtype st1 = new Subtype(), st2 = new Subtype();
        st1.set(st2);
        Subtype st3 = st1.get();
        st1.f();
    }
}  
/* 程式輸出
Subtype
*/
           

新類Subtype接受的參數和傳回的值具有Subtype類型而不僅僅是基類BasicHolder類型。是以自限定類型的本質就是:基類用子類代替其參數。這意味着泛型基類變成了一種其所有子類的公共功能模版,但是在所産生的類中将使用确切類型而不是基類型。是以,Subtype中,傳遞給set()的參數和從get() 傳回的類型都确切是Subtype。

自限定與協變

自限定類型的價值在于它們可以産生協變參數類型——方法參數類型會随子類而變化。其實自限定還可以産生協變傳回類型,但是這并不重要,因為JDK1.5引入了協變傳回類型。

協變傳回類型

下面這段代碼子類接口把基類接口的方法重寫了,傳回更确切的類型。

// CovariantReturnTypes.java
class Base {}
class Derived extends Base {}

interface OrdinaryGetter { 
    Base get();
}

interface DerivedGetter extends OrdinaryGetter {
    Derived get();
}

public class CovariantReturnTypes {
    void test(DerivedGetter d) {
        Derived d2 = d.get();
    }
}
           

繼承自定義類型基類的子類将産生确切的子類型作為其傳回值,就像上面的get()一樣。

// GenericsAndReturnTypes.java
interface GenericsGetter<T extends GenericsGetter<T>> {
    T get();
}

interface Getter extends GenericsGetter<Getter> {}

public class GenericsAndReturnTypes {
    void test(Getter g) {
        Getter result = g.get();
        GenericsGetter genericsGetter = g.get();
    }
}
           

協變參數類型

在非泛型代碼中,參數類型不能随子類型發生變化。方法隻能重載不能重寫。見下面代碼示例。

// OrdinaryArguments.java
class OrdinarySetter {
    void set(Base base) {
        System.out.println("OrdinarySetter.set(Base)");
    }
}

class DerivedSetter extends OrdinarySetter {
    void set(Derived derived) {
        System.out.println("DerivedSetter.set(Derived)");
    }
}

public class OrdinaryArguments {
    public static void main(String[] args) {
        Base base = new Base();
        Derived derived = new Derived();
        DerivedSetter ds = new DerivedSetter();
        ds.set(derived);
        ds.set(base);
    }
}
/* 程式輸出
DerivedSetter.set(Derived)
OrdinarySetter.set(Base)
*/
           

但是,在使用自限定類型時,在子類中隻有一個方法,并且這個方法接受子類型而不是基類型為參數。

interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
    void set(T args);
}

interface Setter extends SelfBoundSetter<Setter> {}

public class SelfBoundAndCovariantArguments {
    void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
        s1.set(s2);
        s1.set(sbs);  // 編譯錯誤
    }
}
           

捕獲轉換

<?>被稱為無界通配符,無界通配符有什麼作用這裡不再詳細說明了,了解了前面東西的同學應該能推斷出來。無界通配符還有一個特殊的作用,如果向一個使用<?>的方法傳遞原生類型,那麼對編譯期來說,可能會推斷出實際的參數類型,使得這個方法可以回轉并調用另一個使用這個确切類型的方法。這種技術被稱為捕獲轉換。下面代碼示範了這種技術。

public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }
    static void f2(Holder<?> holder) {
        f1(holder);
    }
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder<Integer>(1);
        f2(raw);
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic);
        Holder<?> wildcarded = new Holder<Double>(1.0);
        f2(wildcarded);
    }
}
/* 程式輸出
Integer
Object
Double
*/
           

捕獲轉換隻有在這樣的情況下可以工作:即在方法内部,你需要使用确切的類型。注意,不能從f2()中傳回T,因為T對于f2()來說是未知的。捕獲轉換十分有趣,但是非常受限。

繼續閱讀