---------------------------------------------------------------------------------------------
七-2 強(qiáng)制類型轉(zhuǎn)換和instanceof
泛型類在它所有的實(shí)例****享,就意味著判斷一個(gè)實(shí)例是否是一個(gè)特別調(diào)用的泛
型的實(shí)例是毫無(wú)意義的:
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) {...}//非法
類似地,像這樣的強(qiáng)制類型轉(zhuǎn)換:
Collection<String> cstr = (Collection<String>) cs;//未檢測(cè)警告
給出了一個(gè)未檢測(cè)的警告,因?yàn)檫@里系統(tǒng)在運(yùn)行時(shí)并不會(huì)檢測(cè)。
對(duì)于類型變量也一樣:
<T> T BadCast(T t, Object o) {
return (T) o;//未檢測(cè)警告
}
類型變量不存在于運(yùn)行時(shí),這就是說(shuō)它們對(duì)時(shí)間或空間的性能不會(huì)造成影響。
但也因此而不能通過(guò)強(qiáng)制類型轉(zhuǎn)換可靠地使用它們了。
------------------------------------------------------------------------------------------
七-3 數(shù)組
數(shù)組對(duì)象的組件類型可能不是一個(gè)類型變量或一個(gè)參數(shù)化類型,除非它是一個(gè)
(無(wú)界的)通配符類型。你可以聲明元素類型是類型變量和參數(shù)華類型的數(shù)組類型,
但元素類型不能是數(shù)組對(duì)象。
這自然有點(diǎn)郁悶,但這個(gè)限制對(duì)避免下面的情況是必要的:
List<Strign>[] lsa = new List<String>[10];//實(shí)際上是不允許的
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(8));
oa[1] = li;//不合理,但可以通過(guò)運(yùn)行時(shí)的賦值檢測(cè)
String s = lsa[1].get(0);//運(yùn)行時(shí)出錯(cuò):ClassCastException異常
如果參數(shù)化類型的數(shù)組允許的話,那么上面的例子編譯時(shí)就不會(huì)有未檢測(cè)的警告,
但在運(yùn)行時(shí)出錯(cuò)。對(duì)于泛型編程,我們的主要設(shè)計(jì)目標(biāo)是類型安全,而特別的是這個(gè)
語(yǔ)言的設(shè)計(jì)保證了如果使用了javac -source 1.5來(lái)編譯整個(gè)程序而沒(méi)有未檢測(cè)的
警告的話,它是類型安全的。
但是你仍然會(huì)使用通配符數(shù)組,這與上面的代碼相比有兩個(gè)變化。首先是不使用
數(shù)組對(duì)象或元素類型被參數(shù)化的數(shù)組類型,這樣我們就需要在從數(shù)組中取出一個(gè)字符
串的時(shí)候進(jìn)行強(qiáng)制類型轉(zhuǎn)換:
List<?>[] lsa = new List<?>[10];//沒(méi)問(wèn)題,無(wú)界通配符類型數(shù)組
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;//正確
String s = (String) lsa[1].get(0);//運(yùn)行時(shí)錯(cuò)誤,顯式強(qiáng)制類型轉(zhuǎn)換
第二個(gè)變化是,我們不創(chuàng)建元素類型被參數(shù)化的數(shù)組對(duì)象,但仍然使用參數(shù)化元素
類型的數(shù)組類型,這是允許的,但引起現(xiàn)未檢測(cè)警告。這樣的程序?qū)嶋H上是不安全的,
甚至最終會(huì)出錯(cuò)。
List<String>[] lsa = new List<?>[10];//未檢測(cè)警告-這是不安全的!
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<integer>();
li.add(new Integer(3));
oa[1]=li;//正確
String s = lsa[1].get(0);//運(yùn)行出錯(cuò),但之前已經(jīng)被警告
類似地,想創(chuàng)建一個(gè)元素類型是類型變量的數(shù)組對(duì)象的話,將會(huì)編譯出錯(cuò)。
<T> T[] makeArray(T t){
return new T[100];//錯(cuò)誤
}
因?yàn)轭愋妥兞坎⒉淮嬖谟谶\(yùn)行時(shí),所以沒(méi)有辦法知道實(shí)際的數(shù)組類型是什么。
要突破這類限制,我們可以用第8部分說(shuō)到的用類名作為運(yùn)行時(shí)標(biāo)記的方法。
------------------------------------------------------------------------------------------
八、 把類名作為運(yùn)行時(shí)的類型標(biāo)記
JDK1.5中的一個(gè)變化是java.lang.Class是泛化的,一個(gè)有趣的例子是對(duì)
容器外的東西使用泛型。
現(xiàn)在Class類有一個(gè)類型參數(shù)T,你可能會(huì)問(wèn),T代表什么???它就代表Class
對(duì)象所表示的類型。
比如,String.class的類型是Class<String>,Serializable.class的
類型是Class<Serializable>,這可以提高你的反射代碼中的類型安全性。
特別地,由于現(xiàn)在Class類中的newInstance()方法返回一個(gè)T對(duì)象,因此
在通過(guò)反射創(chuàng)建對(duì)象的時(shí)候可以得到更精確的類型。
其中一個(gè)方法就是顯式傳入一個(gè)factory對(duì)象,代碼如下:
interface Factory<T> {T make();}
public <T> Collection<T> select(Factory<T> factory, String statement){
Collection<T> result = new ArrayList<T>();
//用JDBC運(yùn)行SQL查詢
for(/*遍歷JDBC結(jié)果*/){
T item = factory.make();
/*通過(guò)SQL結(jié)果用反射和設(shè)置數(shù)據(jù)項(xiàng)*/
result.add(item);
}
return result;
}
你可以這樣調(diào)用:
select(new Factory<EmpInfo>(){ public EmpInfo make() {
return new EmpInfo();
}}
, "selection string");
或者聲明一個(gè)EmpInfoFactory類來(lái)支持Factory接口:
class EmpInfoFactory implements Factory<EmpInfo>{
...
public EmpInfo make() { return new EmpInfo();}
}
然后這樣調(diào)用:
select(getMyEmpInfoFactory(), "selection string");
這種解決辦法需要下面的其中之一:
· 在調(diào)用的地方使用詳細(xì)的匿名工廠類(verbose anonymous factory classes),或者
· 為每個(gè)使用的類型聲明一個(gè)工廠類,并把工廠實(shí)例傳遞給調(diào)用的地方,這樣有點(diǎn)不自然。
使用類名作為一個(gè)工廠對(duì)象是非常自然的事,這樣的話還可以為反射所用?,F(xiàn)在
沒(méi)有泛型的代碼可能寫作如下:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/*用JDBC執(zhí)行SQL查詢*/
for(/*遍歷JDBC產(chǎn)生的結(jié)果*/){
Object item = c.newInstance();
/*通過(guò)SQL結(jié)果用反射和設(shè)置數(shù)據(jù)項(xiàng)*/
result.add(item);
}
return result;
}
但是,這樣并不能得到我們所希望的更精確的集合類型,現(xiàn)在Class是泛化的,
我們可以這樣寫:
Collection<EmpInfo> emps =
sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) {
Collection<T> result = new ArrayList<T>();
/*用JDBC執(zhí)行SQL查詢*/
for(/*遍歷JDBC產(chǎn)生的結(jié)果*/){
T item = c.newInstance();
/*通過(guò)SQL結(jié)果用反射和設(shè)置數(shù)據(jù)項(xiàng)*/
result.add(item);
}
return result;
}
這樣就通過(guò)類型安全的方法來(lái)得到了精確的集合類型了。
這種使用類名作為運(yùn)行時(shí)類型標(biāo)記的技術(shù)是一個(gè)很有用的技巧,是需要知道的。
在處理注釋的新的API中也有很多類似的情況。
----------------------------------------------------------------------------------------------
九 通配符的其他作用
(more fun with wildcards,不知道如何譯才比較妥當(dāng),呵呵。)
在這部分,我們將會(huì)仔細(xì)看看通配符的幾個(gè)較為深入的用途。我們已經(jīng)從幾個(gè)
有界通配符的例子中看到,它對(duì)從某一數(shù)據(jù)結(jié)構(gòu)中讀取數(shù)據(jù)是很有用的?,F(xiàn)在來(lái)看
看相反的情況,只對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行寫操作。
下面的Sink接口就是這類情況的一個(gè)簡(jiǎn)單的例子:
interface Sink<T> {
flush(T t);
}
我們可以想象在下面的示范的例子中使用它,writeAll()方法用于把coll集合
里的所有元素填充(flush)到Sink接口變量snk中,并返回最后一個(gè)填充的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk){
T last;
for (T t: coll){
last = t;
snk.flush(last);
}
return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s);//非法調(diào)用
如注釋所注,這里對(duì)writeAll()方法的調(diào)用是非法的,因?yàn)闊o(wú)有效的類型參數(shù)
可以引用;String和Object都不適合作為T的類型,因?yàn)镃ollection和Sink的元素
必須是相同類型的。
我們可以通過(guò)使用通配符來(lái)改寫writeAll()的方法頭來(lái)處理,如下:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
String str = writeAll(cs, s);//調(diào)用沒(méi)問(wèn)題,但返回類型錯(cuò)誤
現(xiàn)在調(diào)用是合法的了,但由于T的類型跟元素類型是Object的s一樣,因?yàn)榉祷氐?br />類型也是Object,因此賦值是不正確的。
解決辦法是使用我們之前從未見(jiàn)過(guò)的一種有界通配符形式:帶下界的通配符。
語(yǔ)法 ? super T 表示了是未知的T的父類型,這與我們之前所使用的有界
(父類型:或者T類型本身,要記住的是,你類型關(guān)系是自反的)
通配符是對(duì)偶有界通配符,即用 ? extends T 表示未知的T的子類型。
public static<T> T writeAll(Collection<T> coll, Sink<? super T> snk) {...}
...
String str = writeAll(cs, s);//正確!
使用這個(gè)語(yǔ)法的調(diào)用是合法的,指向的類型是所期望的String類型。
現(xiàn)在我們來(lái)看一個(gè)比較現(xiàn)實(shí)一點(diǎn)的例子,java.util.TreeSet<E>表示元素類型
是E的樹形數(shù)據(jù)結(jié)構(gòu)里的元素是有序的,創(chuàng)建一個(gè)TreeSet對(duì)象的一個(gè)方法是使用參數(shù)
是Comparator對(duì)象的構(gòu)造函數(shù),Comparator對(duì)象用于對(duì)TreeSet對(duì)象里的元素進(jìn)行
所期望的排序進(jìn)行分類。
TreeSet(Comparator<E> c)
Comparator接口是必要的:
interface Comparator<T> {
int compare(T fst, T snd);
}
假設(shè)我們想要?jiǎng)?chuàng)建一個(gè)TreeSet<String>對(duì)象,并傳入一下合適的Comparator
對(duì)象,我們傳遞的Comparator是能夠比較字符串的。我們可以用Comparator<String>,
但Comparator<Object>也是可以的。但是,我們不能對(duì)Comparator<Object>對(duì)象
調(diào)用上面所給的構(gòu)造函數(shù),我們可以用一個(gè)下界通配符來(lái)得到我們想要的靈活性:
TreeSet(Comparator<? super E> c)
這樣就可以使用適合的Comparator對(duì)象啦。
最后一個(gè)下界通配符的例子,我們來(lái)看看Collections.max()方法,這個(gè)方法
返回作為參數(shù)傳遞的Collection對(duì)象中最大的元素。
現(xiàn)在,為了max()方法能正常運(yùn)行,傳遞的Collection對(duì)象中的所有元素都必
須是實(shí)現(xiàn)了Comparable接口的,還有就是,它們之間必須是可比較的。
先試一下泛化方法頭的寫法:
public static <T extends Comparable<T>>
T max(Collection<T> coll)
那樣,方法就接受一個(gè)自身可比較的(comparable)某個(gè)T類型的Collection
對(duì)象,并返回T類型的一個(gè)元素。這樣顯得太束縛了。
來(lái)看看為什么,假設(shè)一個(gè)類型可以與合意的對(duì)象進(jìn)行比較:
class Foo implements Comparable<Object> {...}
...
Collection<Foo> cf = ...;
Collectins.max(cf);//應(yīng)該可以正常運(yùn)行
cf里的每個(gè)對(duì)象都可以和cf里的任意其他元素進(jìn)行比較,因?yàn)槊總€(gè)元素都是Foo
的對(duì)象,而Foo對(duì)象可以與任意的對(duì)象進(jìn)行比較,特別是同是Foo對(duì)象的。但是,使用
上面的方法頭,我們會(huì)發(fā)現(xiàn)這樣的調(diào)用是不被接受的,指向的類型必須是Foo,但Foo
并沒(méi)有實(shí)現(xiàn)Comparable<Foo>。
T對(duì)于自身的可比性不是必須的,需要的是T與其父類型是可比的,就像下面:
(實(shí)際的Collections.max()方法頭在后面的第10部分將會(huì)講得更多)
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
這樣推理出來(lái)的結(jié)果基本上適用于想用Comparable來(lái)用于任意類型的用法:
就是你想這樣用Comparable<? super T>。
總的來(lái)說(shuō),如果你有一個(gè)只能一個(gè)T類型參數(shù)作為實(shí)參的API的話,你就應(yīng)該用
下界通配符類型(? suer T);相反,如果API只返回T對(duì)象,你就應(yīng)該用上界通
配符類型(? extends T),以使得你的客戶的代碼有更大的靈活性。
-------------------------------------------------------------------------------------------
九-1 通配符捕捉(?wildcard capture)
現(xiàn)在應(yīng)該很清楚,給出下面的例子:
Set<?> unknownSet = new HashSet<String>();
...
/** 給Set對(duì)象s添加一個(gè)元素t*/
public static <T> void addToSet<Set<T> s, T t) {...}
下面的調(diào)用是非法的。
addToSet(unknownSet, "abc");//非法的
這無(wú)異于實(shí)際傳遞的Set對(duì)象是一個(gè)字符串類型的Set對(duì)象,問(wèn)題是作為實(shí)參傳遞的
是一個(gè)未知類型的Set對(duì)象,這樣就不能保證它是一個(gè)字符串類型或其他類型的Set對(duì)象。
現(xiàn)在,來(lái)看下面:
class Collections{
...
<T> public static Set<T> unmodifiableSet<Set<T> set) {...}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet);//這是可以的,
//為什么呢?
看起來(lái)這應(yīng)該是不允許的,但是請(qǐng)看看這個(gè)特別的調(diào)用,這完全是安全的,因此
這是允許的。這里的unmodifiableSet()確實(shí)是對(duì)任何類型的Set都適合,不管它的
元素類型是什么。
因?yàn)檫@種情況出現(xiàn)得相對(duì)頻繁,因此就有一個(gè)特殊的規(guī)則,對(duì)代碼能夠被檢驗(yàn)是
安全的任何特定的環(huán)境,那樣的代碼都是允許的。這個(gè)規(guī)則就是所謂的“通配符捕捉”,
允許編譯器對(duì)泛型方法引用未知類型的通配符作為類型實(shí)參。
-------------------------------------------------------------------------------------------------------
十 把遺留代碼轉(zhuǎn)化為泛型代碼
早前,我們展示了如何使泛型代碼和遺留代碼交互,現(xiàn)在該是時(shí)候來(lái)看看更難的
問(wèn)題:把老代碼改為泛型代碼。
如果決定了把老代碼轉(zhuǎn)換為泛型代碼,你必須慎重考慮如何修改你的API。
你不能對(duì)泛型API限制得太死,它得要繼續(xù)支持API的最初約定。再看幾個(gè)關(guān)于
java.util.Collection的例子。非泛型的API就像這樣:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
先這樣簡(jiǎn)單來(lái)嘗試一下泛化:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
這個(gè)當(dāng)然是類型安全的,它沒(méi)有做到API的最初約定,containsAll()方法接受
傳入的任何類型的Collection對(duì)象,只有當(dāng)Collection對(duì)象中只包括E類型的實(shí)例
的時(shí)候才正確。但是:
· 傳入的Collection對(duì)象的靜態(tài)類型可能不同,這樣的原因可能是調(diào)用者不知道
傳入的Collection對(duì)象的精確類型,又或者它是Collection<S>類型的,其中S是E的
子類型。
· 對(duì)不同類型的Collection對(duì)象調(diào)用方法containsAll()完全是合法的,程序
應(yīng)該能夠運(yùn)行,返回的是false值。
對(duì)于addAll()方法這種情況,我們應(yīng)該能夠添加任何存在了E類型的子類型的Collection
對(duì)象,我們?cè)诘?部分中看過(guò)了如何正確處理這種情況。
還要保證改進(jìn)的API能夠保留對(duì)老客戶的二進(jìn)制支持(? binary compatibility)。
這就意味著API“擦除”后(erasure)必須與最初的非泛型API一致。在大多數(shù)的情
況的結(jié)果是自然而然的,但有些小地方卻不盡如此。我們將仔細(xì)去看看我們之前遇到
過(guò)的最小的Collections.max()方法,正如我們?cè)诘?部分所見(jiàn),似乎正確的max()
的方法頭:
public static <T extends Comparable<? super T>>
T max(CollectionT> coll)
基本沒(méi)問(wèn)題,除了方法頭被“擦除”后的情況:
public static Comparable max(Collection coll)
這與max()方法最初的方法頭不一樣:
public static Object max(Collection coll)
本來(lái)是想得到想要的max()方法頭,但是沒(méi)成功,所有老的二進(jìn)制class文件
調(diào)用的Collections.max()都依賴于一個(gè)返回Object類型的方法頭。
我們可以在類型參數(shù)T的邊界中顯式指定一個(gè)父類型來(lái)強(qiáng)制改變“擦除”的結(jié)果。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
這是一個(gè)對(duì)類型參數(shù)給出多個(gè)邊界的例子,語(yǔ)法是這樣:T1 & T2 ... & Tn.
多邊界類型變量對(duì)邊界類型列表中的所有類型的子類型都是可知的,當(dāng)使用多邊界
類型的時(shí)候,邊界類型列表中的第一個(gè)類型將被作為類型變量“擦除”后的類型。
最后,我們應(yīng)該記住max()方法只是從傳入的Collection方法中讀取數(shù)據(jù),
因此也就適用于類型是T的子類型的任何Collection對(duì)象。
這樣就有了我們JDK中實(shí)際的方法頭:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在實(shí)踐中很少會(huì)有涉及到這么多東西的情況,但專業(yè)類型設(shè)計(jì)者在轉(zhuǎn)換現(xiàn)有的API
的時(shí)候應(yīng)該有所準(zhǔn)備的仔細(xì)思慮。
另一個(gè)問(wèn)題就是要小心協(xié)變返回(covariant returns)的情況,那就是改進(jìn)
子類中方法的返回類型。你不應(yīng)該在老API中使用這個(gè)特性。
假設(shè)你最初的API是這樣的:
public class Foo {
public Foo create {...}//工廠方法,應(yīng)該是創(chuàng)建聲明的類的一個(gè)實(shí)例
}
public class Bar extends Foo {
public Foo create() {...}//實(shí)際是創(chuàng)建一個(gè)Bar實(shí)例
}
用協(xié)變返回的話,是這樣改:
public class Foo {
public Foo create {...}//工廠方法,應(yīng)該是創(chuàng)建聲明的類的一個(gè)實(shí)例
}
public class Bar extends Foo {
public Bar create() {...}//實(shí)際是創(chuàng)建一個(gè)Bar實(shí)例
}
現(xiàn)在,假設(shè)有這樣的第三方客戶代碼:
public class Baz extends Bar {
public Foo create() {...} //實(shí)際是創(chuàng)建一個(gè)Baz實(shí)例
}
Java虛擬機(jī)不直接支持不同返回類型的方法的覆蓋,編譯器就是支持這樣的
特性。結(jié)果就是,除非重編譯Baz類,否則的話它不會(huì)正確覆蓋Bar中的create()
方法。此外,Baz類需要修改,因?yàn)樯厦鎸懙拇a不能通過(guò)編譯,Baz中create()
方法的返回類型不是Bar類中create()方法的返回類型的子類型。
十一、鳴謝(這里就不翻譯了)
Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen,
Peter von der Ah′e and Philip Wadler contributed material to this
tutorial.
Thanks to David Biesack, Bruce Chapman, David Flanagan, Neal Gafter,
¨ Orjan Petersson, Scott Seligman, Yoshiki Shibata and Kresten Krab
Thorup for valuable feedback on earlier versions of this tutorial.
Apologies to anyone whom I’ve forgotten.