定義
逆變與協(xié)變用來描述類型轉(zhuǎn)換(type transformation)后的繼承關(guān)系毅桃,其定義:如果A、B表示類型准夷,f(?)表示類型轉(zhuǎn)換钥飞,≤表示繼承關(guān)系(比如,A≤B表示A是由B派生出來的子類)
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)相互之間沒有繼承關(guān)系酒朵。
數(shù)組是協(xié)變的
Java中數(shù)組是協(xié)變的桦锄,可以向子類型的數(shù)組賦予基類型的數(shù)組引用,請(qǐng)看下面代碼蔫耽。
// 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);
}
}
}
main()中第一行創(chuàng)建了一個(gè)Apple數(shù)組结耀,并將其賦值給一個(gè)Fruit數(shù)組引用。編譯器允許你把Fruit放置到這個(gè)數(shù)組中匙铡,這對(duì)于編譯器是有意義的饼记,因?yàn)樗且粋€(gè)Fruit引用——它有什么理由不允許將Fruit對(duì)象或者任何從Fruit繼承出來的對(duì)象(例如Orange),放置到這個(gè)數(shù)組中呢慰枕?
可能有同學(xué)會(huì)疑惑具则,明明Fruit[]引用的是一個(gè)Apple數(shù)組,編譯器看不出來嗎具帮?還允許往里面放Fruit和Orange類的對(duì)象博肋。你要站在編譯器的角度看問題低斋,編譯器可沒有人這么聰明。現(xiàn)代編譯器大多采用的是上下文無關(guān)文法(編譯器:老子歸約一句是一句)匪凡,符號(hào)表中存儲(chǔ)的標(biāo)識(shí)符fruit是Fruit[]類型(不然咱還怎么多態(tài))膊畴,在以后的解析過程中編譯器看到fruit只會(huì)認(rèn)為是Fruit[]類型。
不過病游,盡管編譯器允許了這樣做唇跨,運(yùn)行時(shí)的數(shù)組機(jī)制知道它處理的是Apple[],因此會(huì)在向數(shù)組中放置異構(gòu)類型時(shí)拋出異常衬衬。程序的運(yùn)行結(jié)果如下买猖。
java.lang.ArrayStoreException: generics.Fruit
java.lang.ArrayStoreException: generics.Orange
泛型是不變的
當(dāng)我們使用泛型容器來替代數(shù)組時(shí),看看會(huì)發(fā)生什么滋尉。
public class NonCovariantGenerics {
List<Fruit> flist = new ArrayList<Apple>(); // 編譯錯(cuò)誤
}
直接在編譯時(shí)報(bào)錯(cuò)了玉控。與數(shù)組不同,泛型沒有內(nèi)建的協(xié)變類型狮惜。這是因?yàn)閿?shù)組在語言中是完全定義的高诺,因此內(nèi)建了編譯期和運(yùn)行時(shí)的檢查,但是在使用泛型時(shí)碾篡,類型信息在編譯期被擦除了(如果你不知道什么是擦除虱而,可以去看這篇文章補(bǔ)補(bǔ)課類型擦除),運(yùn)行時(shí)也就無從檢查开泽。因此薛窥,泛型將這種錯(cuò)誤檢測(cè)移入到編譯期。
通配符引入?yún)f(xié)變眼姐、逆變
協(xié)變
Java泛型是不變的诅迷,可有時(shí)需要實(shí)現(xiàn)協(xié)變,在兩個(gè)類型之間建立某種類型的向上轉(zhuǎn)型關(guān)系众旗,怎么辦呢罢杉?這時(shí),通配符派上了用場(chǎng)贡歧。
public class GenericsAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();
flist.add(new Apple()); // 編譯錯(cuò)誤
flist.add(new Fruit()); // 編譯錯(cuò)誤
flist.add(new Object()); // 編譯錯(cuò)誤
}
}
現(xiàn)在flist的類型是<? extends Fruit>滩租,extends指出了泛型的上界為Fruit,<? extends T>稱為子類通配符利朵,意味著某個(gè)繼承自Fruit的具體類型律想。使用通配符可以將ArrayList<Apple>向上轉(zhuǎn)型了,也就實(shí)現(xiàn)了協(xié)變绍弟。
然而技即,事情變得怪異了,觀察上面代碼樟遣,你再也不能往容器里放入任何東西而叼,甚至連Apple都不行身笤。
原因在于,List<? extends Fruit>也可以合法的指向一個(gè)List<Orange>葵陵,顯然往里面放Apple液荸、Fruit、Object都是非法的脱篙。編譯器不知道List<? extends Fruit>所持有的具體類型是什么娇钱,所以一旦執(zhí)行這種類型的向上轉(zhuǎn)型,你就將丟失掉向其中傳遞任何對(duì)象的能力绊困。
類比數(shù)組文搂,盡管你可以把Apple[]向上轉(zhuǎn)型成Fruit[],然而往里面添加Fruit和Orange等對(duì)象都是非法的考抄,會(huì)在運(yùn)行時(shí)拋出ArrayStoreException異常。泛型把類型檢查移到了編譯期蔗彤,協(xié)變過程丟掉了類型信息川梅,編譯器拒絕所有不安全的操作。
逆變
我們還可以走另外一條路然遏,就是逆變贫途。
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); // 編譯錯(cuò)誤
}
}
我們重用了關(guān)鍵字super指出泛型的下界為Apple,<待侵? super T>稱為超類通配符丢早,代表一個(gè)具體類型,而這個(gè)類型是Apple的超類秧倾。這樣編譯器就知道向其中添加Apple或Apple的子類型(例如Jonathan)是安全的了怨酝。但是,既然Apple是下界那先,那么可以知道向這樣的List中添加Fruit是不安全的农猬。
PECS
上面說的可能有點(diǎn)繞,那么總結(jié)下:什么使用extends售淡,什么時(shí)候使用super斤葱。《Effective Java》給出精煉的描述:producer-extends, consumer-super(PECS)揖闸。
說直白點(diǎn)就是揍堕,從數(shù)據(jù)流來看,extends是限制數(shù)據(jù)來源的(生產(chǎn)者)汤纸,而super是限制數(shù)據(jù)流入的(消費(fèi)者)衩茸。例如上面SuperTypeWildcards類里,使用<? super Apple>就是限制add方法傳入的類型必須是Apple及其子類型贮泞。
仿照上面的代碼递瑰,我寫了個(gè)ExtendTypeWildcards類祟牲,可以看出<? extends Apple>限制了get方法返回的類型必須是Apple及其父類型。
public class ExtendTypeWildcards {
static void readFrom(List<? extends Apple> apples) {
Apple apple = apples.get(0);
Jonathan jonathan = apples.get(0); // 編譯錯(cuò)誤
Fruit fruit = apples.get(0);
}
}
例子
框架和庫(kù)代碼中到處都是PECS抖部,下面我們來看一些具體的例子说贝,加深理解。
- java.util.Collections的copy方法
// Collections.java
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());
}
}
}
copy方法限制了拷貝源src必須是T或者是它的子類慎颗,而拷貝目的地dest必須是T或者是它的父類乡恕,這樣就保證了類型的合法性。
- Rxjava的變換
這里我們貼出一小段Rxjava2.0中map函數(shù)的源碼俯萎。
// Observable.java
public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
ObjectHelper.requireNonNull(mapper, "mapper is null");
return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
}
Function函數(shù)將<? super T>類型轉(zhuǎn)變?yōu)?lt;? extends R>類型(類似于代理模式的攔截器)傲宜,可以看出extends和super分別限制輸入和輸出,它們可以是不同類型夫啊。
自限定的類型
理解自限定
Java泛型中函卒,有一個(gè)好像是經(jīng)常性出現(xiàn)的慣用法,它相當(dāng)令人費(fèi)解撇眯。
class SelfBounded<T extends SelfBounded<T>> { // ...
SelfBounded類接受泛型參數(shù)T报嵌,而T由一個(gè)邊界類限定,這個(gè)邊界就是擁有T作為其參數(shù)的SelfBounded熊榛,看起來是一種無限循環(huán)锚国。
先給出結(jié)論:這種語法定義了一個(gè)基類,這個(gè)基類能夠使用子類作為其參數(shù)玄坦、返回類型血筑、作用域。為了理解這個(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
*/
新類Subtype接受的參數(shù)和返回的值具有Subtype類型而不僅僅是基類BasicHolder類型。所以自限定類型的本質(zhì)就是:基類用子類代替其參數(shù)择懂。這意味著泛型基類變成了一種其所有子類的公共功能模版园欣,但是在所產(chǎn)生的類中將使用確切類型而不是基類型。因此休蟹,Subtype中沸枯,傳遞給set()的參數(shù)和從get() 返回的類型都確切是Subtype。
自限定與協(xié)變
自限定類型的價(jià)值在于它們可以產(chǎn)生協(xié)變參數(shù)類型——方法參數(shù)類型會(huì)隨子類而變化赂弓。其實(shí)自限定還可以產(chǎn)生協(xié)變返回類型绑榴,但是這并不重要,因?yàn)镴DK1.5引入了協(xié)變返回類型盈魁。
協(xié)變返回類型
下面這段代碼子類接口把基類接口的方法重寫了翔怎,返回更確切的類型。
// 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();
}
}
繼承自定義類型基類的子類將產(chǎn)生確切的子類型作為其返回值,就像上面的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ù)類型
在非泛型代碼中飘痛,參數(shù)類型不能隨子類型發(fā)生變化。方法只能重載不能重寫容握。見下面代碼示例宣脉。
// 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)
*/
但是,在使用自限定類型時(shí)剔氏,在子類中只有一個(gè)方法塑猖,并且這個(gè)方法接受子類型而不是基類型為參數(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)換
<?>被稱為無界通配符谈跛,無界通配符有什么作用這里不再詳細(xì)說明了羊苟,理解了前面東西的同學(xué)應(yīng)該能推斷出來。無界通配符還有一個(gè)特殊的作用感憾,如果向一個(gè)使用<?>的方法傳遞原生類型蜡励,那么對(duì)編譯期來說,可能會(huì)推斷出實(shí)際的參數(shù)類型阻桅,使得這個(gè)方法可以回轉(zhuǎn)并調(diào)用另一個(gè)使用這個(gè)確切類型的方法凉倚。這種技術(shù)被稱為捕獲轉(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)部占遥,你需要使用確切的類型俯抖。注意输瓜,不能從f2()中返回T,因?yàn)門對(duì)于f2()來說是未知的芬萍。捕獲轉(zhuǎn)換十分有趣尤揣,但是非常受限。