通配符
首先,要展示數(shù)組的一種特殊行為瀑粥,可以向?qū)С鲱?lèi)型的數(shù)組賦予基類(lèi)型的數(shù)組引用恒序。
class Fruit {
}
class Apple extends Fruit {
}
class Jonathan extends Apple {
}
class Orange extends Fruit {
}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruits = new Apple[10];
fruits[0] = new Apple();
fruits[1] = new Jonathan();
try {
fruits[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
try {
fruits[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
}
}
// Outputs
java.lang.ArrayStoreException: com.daidaijie.generices.holder.Fruit
java.lang.ArrayStoreException: com.daidaijie.generices.holder.Orange
main()
的第一行創(chuàng)建了一個(gè)Apple
數(shù)組衷咽,并將其賦值給一個(gè)Fruit
數(shù)組引用。這是有意義的蒜绽,因?yàn)?code>Apple也是一種Fruit
兵罢,所以Apple
數(shù)組應(yīng)該也是一個(gè)Fruit
數(shù)組。
但是,如果實(shí)際的數(shù)組類(lèi)型是Apple[]
滓窍,你應(yīng)該只能在其中放置Apple
或者Apple
的子類(lèi)型,這在編譯期和運(yùn)行時(shí)都可以工作巩那。但是要注意的是吏夯,編譯器允許Fruit
放置到這個(gè)數(shù)組中,這對(duì)于編譯器來(lái)說(shuō)是有意義的即横,因?yàn)樗幸粋€(gè)Fruit
引用——它有什么理由不允許將這個(gè)Fruit
對(duì)象或者任意從Fruit
繼承出來(lái)的對(duì)象(例如Orange
)放置到這個(gè)數(shù)組中呢噪生?因此在編譯期,這是允許的东囚。但是跺嗽,運(yùn)行時(shí)數(shù)組機(jī)制知道它是Apple
,因此會(huì)在向數(shù)組中放置異構(gòu)類(lèi)型時(shí)拋出異常页藻。
實(shí)際上桨嫁,向上轉(zhuǎn)型并不合適用在這里。這里真正做的是將一個(gè)數(shù)組賦值為另一個(gè)數(shù)組份帐。數(shù)組的行為應(yīng)該是它可以持有其他對(duì)象璃吧,這里只是因?yàn)槲覀兡軌蛳蛏限D(zhuǎn)型而已,所以很明顯废境,數(shù)組對(duì)象可以保留有關(guān)它們包含的對(duì)象類(lèi)型的規(guī)則畜挨。就好像數(shù)組對(duì)它們持有的對(duì)象是有意識(shí)的,因?yàn)樵诰幾g期檢查和運(yùn)行時(shí)檢查之間噩凹,你不能濫用它們巴元。
對(duì)數(shù)組的這種賦值并不是那么可怕,因?yàn)樵谶\(yùn)行時(shí)可以發(fā)現(xiàn)你已經(jīng)插入不正確的類(lèi)型驮宴,但是泛型的主要目的是將這種錯(cuò)誤檢測(cè)能夠移入到編譯期逮刨。因此當(dāng)試圖使用泛型容器來(lái)代替數(shù)組的時(shí)候,會(huì)發(fā)生什么呢幻赚。
public class NonCovariantGenerics {
// compile error
List<Fruit> flist = new ArrayList<Apple>;
}
第一次看這段代碼的時(shí)候會(huì)認(rèn)為,"不能講一個(gè)Apple
容器賦值給一個(gè)Fruit
容器"禀忆。但是,泛型不僅和容器相關(guān)正確的說(shuō)法是落恼,"不能把一個(gè)涉及Apple
的泛型賦值給一個(gè)涉及Fruit
的泛型"箩退。如果就像在數(shù)組的情況一樣,編譯器對(duì)代碼的了解足夠多佳谦,就可以確定所涉及到的容器戴涝,,那么它可能會(huì)留下一些余地。但是它不知道任何有關(guān)這方面的信息啥刻,因此她拒絕向上轉(zhuǎn)型奸鸯。然而這根本不是向上轉(zhuǎn)型——Apple
的List
不是Fruit
的List
。Apple
的List
將持有Apple
的子類(lèi)型可帽,而Fruit
將持有任何類(lèi)型的Fruit
,誠(chéng)然娄涩,這包括Apple
在內(nèi),但是它不是一個(gè)Apple
的List
映跟,它仍舊是Fruit
的List
蓄拣。Apple
的List
在類(lèi)型上不等價(jià)于Fruit
的List
,即使Apple
是一種Fruit
類(lèi)型努隙。
而真正的問(wèn)題是在談?wù)撊萜鞯念?lèi)型球恤,而不是容器持有的類(lèi)型。與數(shù)組不同荸镊,泛型沒(méi)有內(nèi)建的協(xié)變類(lèi)型咽斧。這是因?yàn)閿?shù)組在語(yǔ)言中是完全定義的,因此內(nèi)建了編譯期和運(yùn)行期的檢查躬存,但是在使用泛型時(shí)张惹,編譯器和運(yùn)行時(shí)系統(tǒng)都不知道你想用類(lèi)型干什么,以及應(yīng)該采用什么樣的規(guī)則优构。
但是有時(shí)候诵叁,想在兩個(gè)類(lèi)型之間建立某種向上轉(zhuǎn)型的關(guān)系,這正是通配符允許的钦椭。
public class GenericsAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<>();
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); //合法但是沒(méi)有意義
// 我們至少知道這會(huì)返回Fruit類(lèi)型
Fruit f = flist.get(0);
}
}
flist
類(lèi)型現(xiàn)在是List<? extends Fruit>
拧额,可以將其讀作"具有任何從Fruit
繼承的類(lèi)型的類(lèi)型的列表",但是這實(shí)際上并不意味著這個(gè)List
將持有任何類(lèi)型的Fruit
彪腔。通配符引用的是明確的類(lèi)型侥锦,一次它意味著"某種flist
引用沒(méi)有指定具體的類(lèi)型"。因此這個(gè)被賦值的List
必須持有諸如Fruit
或者Apple
這樣的某種指定的類(lèi)型德挣,但是為了向上轉(zhuǎn)型為flist
恭垦,這個(gè)類(lèi)型是什么沒(méi)人關(guān)心。
如果唯一的限制是這個(gè)List
要持有某種具體的Fruit
或者Fruit
的子類(lèi)型格嗅,但是實(shí)際上并不關(guān)心它是什么番挺,那么可以用這樣的List
做什么呢?如果不知道List
要持有什么類(lèi)型屯掖,那么怎么樣才能向其中安全地添加對(duì)象呢玄柏,就像在CovariantArrays.java
中向上轉(zhuǎn)型數(shù)組一樣,所以答案是不能贴铜,除非編譯器而不是運(yùn)行時(shí)系統(tǒng)可以阻止這種操作的發(fā)生粪摘,但是很快會(huì)發(fā)現(xiàn)這一問(wèn)題瀑晒。
現(xiàn)在事情有點(diǎn)極端了,因?yàn)椴荒芟騽倓偮暶鬟^(guò)將持有Apple
對(duì)象的List
放置一個(gè)Apple
對(duì)象了徘意。但是編譯器并不知道這一點(diǎn)苔悦。List<?extends Fruit>
可以合法地指向一個(gè)List<Orange>
椎咧。因此一旦執(zhí)行了這種類(lèi)型的向上轉(zhuǎn)型玖详,就會(huì)丟失掉向其中傳遞任何對(duì)象的能力,就算是傳遞Object
也不行勤讽。
但另一方面竹宋,如果調(diào)用的是一個(gè)返回Fruit
的方法,則是安全的地技,因?yàn)檫@個(gè)List
中任何的對(duì)象至少具有Fruit
類(lèi)型,因此編譯器允許這樣做秒拔。
“智能”的編譯器
編譯器不一定會(huì)阻止通配符修飾的泛型類(lèi)中莫矗,調(diào)用任何接受參數(shù)的方法。
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0); // no warning
flist.contains(new Apple()); // args is 'Object'
flist.indexOf(new Apple()); // args is 'Object'
}
}
上面代碼中砂缩,對(duì)contains()
和indexOf()
的調(diào)用作谚,這兩個(gè)方法都接受Apple
對(duì)象作為參數(shù),而這些調(diào)用都可以正常執(zhí)行庵芭。這不是因?yàn)榫幾g器會(huì)去檢查代碼妹懒,以查看特定的方法是否修改了某個(gè)對(duì)象,而是因?yàn)?code>contains()和indexOf()
將接受Object
類(lèi)型的參數(shù)双吆。而add()
卻是接收了一個(gè)具有泛型參數(shù)類(lèi)型的參數(shù)眨唬,因?yàn)楫?dāng)指定一個(gè)ArrayList<? extends Fruits>
時(shí),add()
的參數(shù)就變成了好乐?Extends Fruits
匾竿,從這個(gè)描述中編譯器并不能了解到這里需要Fruits
的哪個(gè)具體子類(lèi)型,因此它也不會(huì)接收任何類(lèi)型的Fruit
蔚万。即使是將Apple
向上轉(zhuǎn)型為Fruit
岭妖,也無(wú)關(guān)緊要——編譯器將直接拒絕對(duì)參數(shù)列表設(shè)計(jì)通配符的方法(例如add()
的調(diào)用)。
在調(diào)用contains()
和indexOf()
時(shí)反璃,參數(shù)類(lèi)型是Object
昵慌,因此不涉及任何通配符,而編譯器也將允許這個(gè)調(diào)用淮蜈。
所以這意味著斋攀,這將由泛型類(lèi)的設(shè)計(jì)者來(lái)決定哪種調(diào)用是“安全的”,并使用Object
作為其參數(shù)類(lèi)型礁芦。而為了在類(lèi)型中使用通配符的情況下禁止這類(lèi)調(diào)用蜻韭,我們需要在參數(shù)中使用類(lèi)型參數(shù)悼尾。
可以在一個(gè)簡(jiǎn)單的Holder
類(lèi)中看到這一點(diǎn)。
public class Holder<T> {
private T value;
public Holder() {
}
public Holder(T value) {
this.value = value;
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> apple = new Holder<>(new Apple());
Apple d = apple.get();
// can't not upcast
// Holder<Fruit> fruit = apple;
Holder<? extends Fruit> fruit = apple; // Ok
Fruit p = fruit.get();
try {
Orange c = (Orange) fruit.get();
} catch (Exception e) {
System.out.println(e);
// fruit.set(new Apple()); Can't call set()
// fruit.set(new Fruit); Can't call set()
System.out.println(fruit.equals(d)); // Ok
}
}
}
// Outputs
java.lang.ClassCastException: com.daidaijie.generices.holder.Apple cannot be cast to com.daidaijie.generices.holder.Orange
true
Holder
有一個(gè)接受T
類(lèi)型對(duì)象的set()
方法肖方,和一個(gè)get()
方法闺魏,以及一個(gè)接受Object
對(duì)象的equals()
方法。
可以看到代碼中俯画,Holder<Apple>
不能向上轉(zhuǎn)型為Holder<Fruits>
析桥,但是可以向上轉(zhuǎn)型為Holder<? extends Fruits>
艰垂。如果調(diào)用get()
泡仗,它只會(huì)返回一個(gè)Fruit
——這就是在給定“任何擴(kuò)展自Fruit的對(duì)象”這一邊界之后,它所能知道的一切猜憎。如果能夠了解更多的信息娩怎,那么可以轉(zhuǎn)型到某種具體的Fruit
類(lèi)型,而這不會(huì)調(diào)至任何的警告胰柑,但是存在得到ClassCastException
的風(fēng)險(xiǎn)截亦。set()
方法不能工作于Apple
或Fruit
,因?yàn)?code>set的參數(shù)也是“? extends Fruit”柬讨,這意味它可以是任何事物崩瓤,而編譯器無(wú)法驗(yàn)證“任何事物”的類(lèi)型安全性。
但是踩官,equals()
方法工作良好却桶,因?yàn)樗鼘⒔邮?code>Object類(lèi)型而并非T
類(lèi)型的參數(shù)。因此蔗牡,編譯器只關(guān)注傳遞進(jìn)來(lái)和要返回的對(duì)象類(lèi)型颖系,它并不會(huì)分析代碼,以查看是否執(zhí)行了任何實(shí)際的寫(xiě)入和讀取操作辩越。