泛型的协变与逆变
看下面一段代碼:
Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); //type mismatchList<? extends Number> list = new ArrayList<Number>(); list.add(new Integer(1)); //error list.add(new Float(1.2f)); //errorInteger是Number的子類(lèi),Integer類(lèi)型的實(shí)例可以賦值給Number類(lèi)型的變量,為什么ArrayList<Integer>不可以賦值給ArrayList<Number>?這需要我們了解Java中的泛型通配符以及協(xié)變與逆變。
協(xié)變與逆變
Liskov替換原則
所有引用基類(lèi)(父類(lèi))的地方必須能透明地使用其子類(lèi)的對(duì)象。
LSP包含以下四層含義:
- 子類(lèi)完全擁有父類(lèi)的方法,且具體子類(lèi)必須實(shí)現(xiàn)父類(lèi)的抽象方法。
- 子類(lèi)中可以增加自己的方法。
- 當(dāng)子類(lèi)覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí),方法的形參要比父類(lèi)方法的更為寬松。
- 當(dāng)子類(lèi)覆蓋或?qū)崿F(xiàn)父類(lèi)的方法時(shí),方法的返回值要比父類(lèi)更嚴(yán)格。
定義
逆變與協(xié)變用來(lái)描述類(lèi)型轉(zhuǎn)換(type transformation)后的繼承關(guān)系,其定義:如果A、B表示類(lèi)型,f(?)表示類(lèi)型轉(zhuǎn)換,≤表示繼承關(guān)系(比如,A≤B表示A是由B派生出來(lái)的子類(lèi))
- f(?)是逆變(contravariant)的,當(dāng)A≤B時(shí)有f(B)≤f(A)成立;
- f(?)是協(xié)變(covariant)的,當(dāng)A≤B時(shí)有f(A)≤f(B)成立;
- f(?)是不變(invariant)的,當(dāng)A≤B時(shí)上述兩個(gè)式子均不成立,即f(A)與f(B)相互之間沒(méi)有繼承關(guān)系。
類(lèi)型協(xié)變性
數(shù)組是協(xié)變的
// 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數(shù)組在編譯期間是可以編譯的。但是在運(yùn)行期間會(huì)出異常。因?yàn)閒ruit[0]是Apple類(lèi)型的,在賦值為Orange類(lèi)型時(shí)出異常。
泛型是不變的
方法
調(diào)用方法result = method(n);根據(jù)Liskov替換原則,傳入形參n的類(lèi)型應(yīng)為method形參的子類(lèi)型,即typeof(n)≤typeof(method's parameter);result應(yīng)為method返回值的基類(lèi)型,即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中,子類(lèi)覆蓋(override)父類(lèi)方法時(shí),形參與返回值的類(lèi)型必須與父類(lèi)保持一致:
class Super {
??? Number method(Number n) { ... }
}
class Sub extends Super {
??? @Override
??? Number method(Number n) { ... }
}
從Java 1.5開(kāi)始,子類(lèi)覆蓋父類(lèi)方法時(shí)允許協(xié)變返回更為具體的類(lèi)型:
class Super {
??? Number method(Number n) { ... }
}
class Sub extends Super {
??? @Override
??? Integer method(Number n) { ... }
}
通配符引入?yún)f(xié)變、逆變
Java中泛型是不變的,可有時(shí)需要實(shí)現(xiàn)逆變與協(xié)變,怎么辦呢?這時(shí),通配符?派上了用場(chǎng):
<? extends>實(shí)現(xiàn)了泛型的協(xié)變,比如:
List<? extends Number> list = new ArrayList<Integer>();
<? super>實(shí)現(xiàn)了泛型的逆變,比如:
List<? super Number> list = new ArrayList<Object>();
extends與super
為什么(開(kāi)篇代碼中)List<? extends Number> list在add Integer和Float會(huì)發(fā)生編譯錯(cuò)誤?首先,我們看看add的實(shí)現(xiàn):
public interface List<E> extends Collection<E> {
??? boolean add(E e);
}
在調(diào)用add方法時(shí),泛型E自動(dòng)變成了<? extends Number>,其表示list所持有的類(lèi)型為在Number與Number派生子類(lèi)中的某一類(lèi)型,其中包含Integer類(lèi)型卻又不特指為Integer類(lèi)型(Integer像個(gè)備胎一樣!!!),故add Integer時(shí)發(fā)生編譯錯(cuò)誤。為了能調(diào)用add方法,可以用super關(guān)鍵字實(shí)現(xiàn):
List<? super Number> list = new ArrayList<Object>();
list.add(new Integer(1));
list.add(new Float(1.2f));
<? super Number>表示list所持有的類(lèi)型為在Number與Number的基類(lèi)中的某一類(lèi)型,其中Integer與Float必定為這某一類(lèi)型的子類(lèi);所以add方法能被正確調(diào)用。從上面的例子可以看出,extends確定了泛型的上界,而super確定了泛型的下界。
PECS
現(xiàn)在問(wèn)題來(lái)了:究竟什么時(shí)候用extends什么時(shí)候用super呢?《Effective Java》給出了答案:
PECS: producer-extends, consumer-super.
如果類(lèi)型形參表示一個(gè)T生產(chǎn)者,就使用<? extends T>,如果表示一個(gè)消費(fèi)者,就使用<? super T>。
?
比如,一個(gè)簡(jiǎn)單的Stack API:
public class Stack<E>{public Stack();public void push(E e):public E pop();public boolean isEmpty(); }要實(shí)現(xiàn)pushAll(Iterable<E> src)方法,將src的元素逐一入棧:
public void pushAll(Iterable<E> src){for(E e : src)push(e) }假設(shè)有一個(gè)實(shí)例化Stack<Number>的對(duì)象stack,src有Iterable<Integer>與?Iterable<Float>;在調(diào)用pushAll方法時(shí)會(huì)發(fā)生type mismatch錯(cuò)誤,因?yàn)镴ava中泛型是不可變的,Iterable<Integer>與?Iterable<Float>都不是Iterable<Number>的子類(lèi)型。因此,應(yīng)改為
// Wildcard type for parameter that serves as an E producer public void pushAll(Iterable<? extends E> src) {for (E e : src)push(e); }要實(shí)現(xiàn)popAll(Collection<E> dst)方法,將Stack中的元素依次取出add到dst中,如果不用通配符實(shí)現(xiàn):
// popAll method without wildcard type - deficient! public void popAll(Collection<E> dst) {while (!isEmpty())dst.add(pop()); }同樣地,假設(shè)有一個(gè)實(shí)例化Stack<Number>的對(duì)象stack,dst為Collection<Object>;調(diào)用popAll方法是會(huì)發(fā)生type mismatch錯(cuò)誤,因?yàn)镃ollection<Object>不是Collection<Number>的子類(lèi)型。因而,應(yīng)改為:
// Wildcard type for parameter that serves as an E consumer public void popAll(Collection<? super E> dst) {while (!isEmpty())dst.add(pop()); }在上述例子中,在調(diào)用pushAll方法時(shí)生產(chǎn)了E 實(shí)例(produces E instances),在調(diào)用popAll方法時(shí)dst消費(fèi)了E 實(shí)例(consumes E instances)。Naftalin與Wadler將PECS稱(chēng)為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總結(jié):
- 要從泛型類(lèi)取數(shù)據(jù)時(shí),用extends;
- 要往泛型類(lèi)寫(xiě)數(shù)據(jù)時(shí),用super;
- 既要取又要寫(xiě),就不用通配符(即extends與super都不用)。
?
自限定的類(lèi)型
理解自限定
Java泛型中,有一個(gè)好像是經(jīng)常性出現(xiàn)的慣用法,它相當(dāng)令人費(fèi)解。
class SelfBounded<T extends SelfBounded<T>> { // ...SelfBounded類(lèi)接受泛型參數(shù)T,而T由一個(gè)邊界類(lèi)限定,這個(gè)邊界就是擁有T作為其參數(shù)的SelfBounded,看起來(lái)是一種無(wú)限循環(huán)。
先給出結(jié)論:這種語(yǔ)法定義了一個(gè)基類(lèi),這個(gè)基類(lèi)能夠使用子類(lèi)作為其參數(shù)、返回類(lèi)型、作用域。為了理解這個(gè)含義,我們從一個(gè)簡(jiǎn)單的版本入手。
// 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 */新類(lèi)Subtype接受的參數(shù)和返回的值具有Subtype類(lèi)型而不僅僅是基類(lèi)BasicHolder類(lèi)型。所以自限定類(lèi)型的本質(zhì)就是:基類(lèi)用子類(lèi)代替其參數(shù)。這意味著泛型基類(lèi)變成了一種其所有子類(lèi)的公共功能模版,但是在所產(chǎn)生的類(lèi)中將使用確切類(lèi)型而不是基類(lèi)型。因此,Subtype中,傳遞給set()的參數(shù)和從get() 返回的類(lèi)型都確切是Subtype。
自限定與協(xié)變
自限定類(lèi)型的價(jià)值在于它們可以產(chǎn)生協(xié)變參數(shù)類(lèi)型——方法參數(shù)類(lèi)型會(huì)隨子類(lèi)而變化。其實(shí)自限定還可以產(chǎn)生協(xié)變返回類(lèi)型,但是這并不重要,因?yàn)镴DK1.5引入了協(xié)變返回類(lèi)型。
協(xié)變返回類(lèi)型
下面這段代碼子類(lèi)接口把基類(lèi)接口的方法重寫(xiě)了,返回更確切的類(lèi)型。
// 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();} }繼承自定義類(lèi)型基類(lèi)的子類(lèi)將產(chǎn)生確切的子類(lèi)型作為其返回值,就像上面的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();} }協(xié)變參數(shù)類(lèi)型
在非泛型代碼中,參數(shù)類(lèi)型不能隨子類(lèi)型發(fā)生變化。方法只能重載不能重寫(xiě)。見(jiàn)下面代碼示例。
// 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) */但是,在使用自限定類(lèi)型時(shí),在子類(lèi)中只有一個(gè)方法,并且這個(gè)方法接受子類(lèi)型而不是基類(lèi)型為參數(shù)。
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); // 編譯錯(cuò)誤} }捕獲轉(zhuǎn)換
<?>被稱(chēng)為無(wú)界通配符,無(wú)界通配符有什么作用這里不再詳細(xì)說(shuō)明了,理解了前面東西的同學(xué)應(yīng)該能推斷出來(lái)。無(wú)界通配符還有一個(gè)特殊的作用,如果向一個(gè)使用<?>的方法傳遞原生類(lèi)型,那么對(duì)編譯期來(lái)說(shuō),可能會(huì)推斷出實(shí)際的參數(shù)類(lèi)型,使得這個(gè)方法可以回轉(zhuǎn)并調(diào)用另一個(gè)使用這個(gè)確切類(lèi)型的方法。這種技術(shù)被稱(chēng)為捕獲轉(zhuǎn)換。下面代碼演示了這種技術(shù)。
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 */捕獲轉(zhuǎn)換只有在這樣的情況下可以工作:即在方法內(nèi)部,你需要使用確切的類(lèi)型。注意,不能從f2()中返回T,因?yàn)門(mén)對(duì)于f2()來(lái)說(shuō)是未知的。捕獲轉(zhuǎn)換十分有趣,但是非常受限。
總結(jié)
- 上一篇: 反射-Class
- 下一篇: hamcrest详细介绍