六-1 在泛型代碼中使用遺留代碼
[url=http://xoj.blogone.net][url]
當你在享受在代碼中使用泛型帶來的好處的時候,你怎么樣使用遺留代碼呢?
假設這樣一個例子,你要使用com.Foodlibar.widgets這個包。Fooblibar.com
的人要銷售一個庫存控制系統,主要部分如下:
package com.Fooblibar.widgets;
public interface Part { ... }
public class Inventory {
/**
*Adds a new Assembly to the inventory databse.
*The assembly is given the name name, and consists of a set
*parts specified by parts. All elements of the collection parts
*must support the Part interface.
**/
public static void addAssembly(String name, Collection parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly{
Collection getParts();//Returns a collection of Parts
}
現在,你可以用上面的API來增加新的代碼,它可以很好的保證你調用參數恰當
的addAssembly()方法,就是說傳遞的集合是一個Part類型的Collection對象,當
然,泛型是最適合做這個:
package com.mycompany.inventory;
import com.Fooblibar.widgets.*;
public class Blade implements Part{
...
}
public class Guillotine implements Part {
}
public class Main {
public static void main(Sring[] args) {
Collection<Part> c = new ArrayList<Part>();
c.add(new Guillotine());
c.add(new Blade());
Inventory.addAssembly("thingee", c);
Collection<Part> k = Inventory.getAssembly("thingee").getParts();
}
}
當我們調用addAssembly方法的時候,它想要的第二個參數是Collection類型的,
實參是Collection<Part>類型,但卻可以,為什么呢?畢竟,大多數集合存儲的都不是
Part對象,所以總的來說,編譯器不會知道Collection存儲的是什么類型的集合。
在正規的泛型代碼里面,Collection都帶有類型參數。當一個像Collection這樣
的泛型不帶類型參數使用的時候,稱之為原生類型。
很多人的第一直覺是Collection就是指Collection<Object>,但從我們先前所
看到的可以知道,當需要的對象是Collection<Object>,而傳遞的卻是Collection<Part>
對象的時候,是類型不安全的。確切點的說法是Collection類型表示一個未知類型的
集合,就像Collection<?>。
稍等一下,那樣做也是不正確的!考慮一下調用getParts()方法,它返回一個
Collection對象,然后賦值給k,而k是Collection<Part>類型的;如果調用的結果
是返回一個Collection<?>的對象,這個賦值可能是錯誤的。
事實上,這個賦值是允許的,只是它會產生一個未檢測警告。警告是需要的,因為
編譯器不能保證賦值的正確性。我們沒有辦法通過檢測遺留代碼中的getAssembly()方法
來保證返回的集合的確是一個類型參數是Part的集合。程序里面的類型是Collection,
我們可以合法的對此集合插入任何對象。
所以,這不應該是錯誤的嗎?理論上來說,答案是:是;但實際上如果是泛型代碼
調用遺留代碼的話,這又是允許的。對這個賦值是否可接受,得取決于程序員自己,在
這個例子中賦值是安全的,因為getAssembly()方法約定是返回以Part作為類型參數的
集合,盡管在類型標記中沒有表明。
所以原生類型很像通配符類型,但它們沒有那么嚴格的類型檢測。這是有意設計成
這樣的,從而可以允許泛型代碼可以與之前已有的遺留代碼交互。
在泛型代碼中調用遺留代碼固然是危險的,一旦把泛型代碼和非泛型代碼混合在一
起,泛型系統所提供的全部安全保證就都變得無效了。但這仍比根本不使用泛型要好,
最起碼你知道你的代碼是一致的。
泛型代碼出現的今天,仍然有很多非泛型代碼,二者混合同時使用是不可避免的。
如果一定要把遺留代碼與泛型代碼混合使用,請小心留意那些未檢測警告。仔細的
想想如何才能判定引發警告的代碼是安全的。
如果仍然出錯,代碼引發的警告實際不是類型安全的,那又怎么樣呢?我們會看
那樣的情況,接下來,我們將會部分的觀察編譯器的工作方式。
六-2 擦除和翻譯
public String loophole(Integer x){
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x);//編譯時未檢測警告
return ys.iterator().next();
}
在這里我們定義了一個字符串類型的鏈表和一個一般的老式鏈表,我們先插入
一個Integer對象,然后試圖取出一個String對象,很明顯這是錯誤的。如果我們
忽略警告繼續執行代碼的話,程序將會在我們使用錯誤類型的地方出錯。在運行時,
代碼執行大致如下:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return (String)ys.iterator().next();//運行時出錯
}
當我們要從鏈表中取出一個元素,并把它當作是一個字符串對象而把它轉換為
String類型的時候,我們將會得到一個ClassCastException類型轉換異常。在
泛型版本的loophole()方法里面發生的就是這種情況。
出現這種情況的原因是,Java的泛型是通過一個前臺轉換“擦除”的編譯器實現
的,你基本上可以認為它是一個源碼對源碼的翻譯,這就是為何泛型版的loophole()
方法轉變為非泛型版本的原因。
結果是,Java虛擬機的類型安全性和完整性永遠不會有問題,就算出現未檢測
的警告。
基本上,擦除會除去所有的泛型信息。尖括號里面的所有類型信息都會去掉,比
如,參數化類型的List<String>會轉換為List。類型變量在之后使用時會被類型
變量的上界(通常是Object)所替換。當最后代碼不是類型正確的時候,就會加入
一個適當的類型轉換,就像loophole()方法的最后一行。
對“擦除”的完整描述不是本指南的范圍內的內容,但前面我們所給的簡單描述
也差不多是那樣了。了解這點很有好處,特別是當你想做諸如把現有API轉為使用
泛型(請看第10部分)這樣復雜的東西,或者是想知道為什么它們會那樣的時候。
六-3 在遺留代碼中使用泛型
現在我們來看看相反的情況。假設Fooblibar.com把他們的API轉換為泛型的,
但有些客戶還沒有轉換。代碼就會像下面的:
package com.Fooblibar.widgets;
public interface Part { ... }
publlic class Inventory {
/**
*Adds a new Assembly to the inventory database.
*The assembly is given the name name, and consists of a set
*parts specified by parts. All elements of the collection parts
*must support the Part interface.
**/
public static void addAssembly(String name, Collection<Part> parts) {...}
public static Assembly getAssembly(String name){ ... }
}
public interface Assembly {
Collection<Part> getParts();//Return a collection of Parts
}
客戶代碼如下:
package com.mycompany.inventory;
import com.Fooblibar.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
...
}
public class Main {
public static void main(String[] args){
Collection c = new ArrayList();
c.add(new Guillotine());
c.add(new Blade());
Inventory.addAssembly("thingee", c);//1: unchecked warning
Collection k = Inventory.getAssembly("thingee").getParts();
}
}
客戶代碼是在引進泛型之前寫下的,但是它使用了com.Fooblibar.widgets包和集
合庫,兩個現在都是在用泛型的。在客戶代碼里面使用的泛型全部都是原生類型。
第1行產生一個未檢測警告,因為把一個原生Collection傳遞給了一個需要Part類型的
Collection的地方,編譯器不能保證原生的Collection是一個Part類型的Collection。
不這樣做的話,你也可以在編譯客戶代碼的時候使用source 1.4這個標記來保證不
會產生警告。但是這樣的話你就不能使用所有JDK 1.5引入的新的語言特性。
七、晦澀難懂的部分
七-1 泛型類為所有調用所共享
下面的代碼段會打印出什么呢?
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能會說是false,但是你錯了,打印的是true,因為所有泛型類的實例它們
的運行時的類(run-time class)都是一樣的,不管它們實際類型參數如何。
泛型類之所以為泛型的,是因為它對所有可能的類型參數都有相同的行為,相同
的類可以看作是有很多不同的類型。
結果就是,一個類的靜態的變量和方法也共享于所有的實例中,這就是為什么不
允許在靜態方法或初始化部分、或者在靜態變量的聲明或初始化中引用類型參數。
七-2 強制類型轉換和instanceof
泛型類在它所有的實例中共享,就意味著判斷一個實例是否是一個特別調用的泛
型的實例是毫無意義的:
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) {...}//非法
類似地,像這樣的強制類型轉換:
Collection<String> cstr = (Collection<String>) cs;//未檢測警告
給出了一個未檢測的警告,因為這里系統在運行時并不會檢測。
對于類型變量也一樣:
<T> T BadCast(T t, Object o) {
return (T) o;//未檢測警告
}
類型變量不存在于運行時,這就是說它們對時間或空間的性能不會造成影響。
但也因此而不能通過強制類型轉換可靠地使用它們了。
七-3 數組
數組對象的組件類型可能不是一個類型變量或一個參數化類型,除非它是一個
(無界的)通配符類型。你可以聲明元素類型是類型變量和參數華類型的數組類型,
但元素類型不能是數組對象。
這自然有點郁悶,但這個限制對避免下面的情況是必要的:
List<Strign>[] lsa = new List<String>[10];//實際上是不允許的
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(8));
oa[1] = li;//不合理,但可以通過運行時的賦值檢測
String s = lsa[1].get(0);//運行時出錯:ClassCastException異常
如果參數化類型的數組允許的話,那么上面的例子編譯時就不會有未檢測的警告,
但在運行時出錯。對于泛型編程,我們的主要設計目標是類型安全,而特別的是這個
語言的設計保證了如果使用了javac -source 1.5來編譯整個程序而沒有未檢測的
警告的話,它是類型安全的。
但是你仍然會使用通配符數組,這與上面的代碼相比有兩個變化。首先是不使用
數組對象或元素類型被參數化的數組類型,這樣我們就需要在從數組中取出一個字符
串的時候進行強制類型轉換:
List<?>[] lsa = new List<?>[10];//沒問題,無界通配符類型數組
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);//運行時錯誤,顯式強制類型轉換
第二個變化是,我們不創建元素類型被參數化的數組對象,但仍然使用參數化元素
類型的數組類型,這是允許的,但引起現未檢測警告。這樣的程序實際上是不安全的,
甚至最終會出錯。
List<String>[] lsa = new List<?>[10];//未檢測警告-這是不安全的!
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);//運行出錯,但之前已經被警告
類似地,想創建一個元素類型是類型變量的數組對象的話,將會編譯出錯。
<T> T[] makeArray(T t){
return new T[100];//錯誤
}
因為類型變量并不存在于運行時,所以沒有辦法知道實際的數組類型是什么。
要突破這類限制,我們可以用第8部分說到的用類名作為運行時標記的方法。
八、 把類名作為運行時的類型標記
JDK1.5中的一個變化是java.lang.Class是泛化的,一個有趣的例子是對
容器外的東西使用泛型。
現在Class類有一個類型參數T,你可能會問,T代表什么啊?它就代表Class
對象所表示的類型。
比如,String.class的類型是Class<String>,Serializable.class的
類型是Class<Serializable>,這可以提高你的反射代碼中的類型安全性。
特別地,由于現在Class類中的newInstance()方法返回一個T對象,因此
在通過反射創建對象的時候可以得到更精確的類型。
其中一個方法就是顯式傳入一個factory對象,代碼如下:
interface Factory<T> {T make();}
public <T> Collection<T> select(Factory<T> factory, String statement){
Collection<T> result = new ArrayList<T>();
//用JDBC運行SQL查詢
for(/*遍歷JDBC結果*/){
T item = factory.make();
/*通過SQL結果用反射和設置數據項*/
result.add(item);
}
return result;
}
你可以這樣調用:
select(new Factory<EmpInfo>(){ public EmpInfo make() {
return new EmpInfo();
}}
, "selection string");
或者聲明一個EmpInfoFactory類來支持Factory接口:
class EmpInfoFactory implements Factory<EmpInfo>{
...
public EmpInfo make() { return new EmpInfo();}
}
然后這樣調用:
select(getMyEmpInfoFactory(), "selection string");
這種解決辦法需要下面的其中之一:
· 在調用的地方使用詳細的匿名工廠類(verbose anonymous factory classes),或者
· 為每個使用的類型聲明一個工廠類,并把工廠實例傳遞給調用的地方,這樣有點不自然。
使用類名作為一個工廠對象是非常自然的事,這樣的話還可以為反射所用。現在
沒有泛型的代碼可能寫作如下:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/*用JDBC執行SQL查詢*/
for(/*遍歷JDBC產生的結果*/){
Object item = c.newInstance();
/*通過SQL結果用反射和設置數據項*/
result.add(item);
}
return result;
}
但是,這樣并不能得到我們所希望的更精確的集合類型,現在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執行SQL查詢*/
for(/*遍歷JDBC產生的結果*/){
T item = c.newInstance();
/*通過SQL結果用反射和設置數據項*/
result.add(item);
}
return result;
}
這樣就通過類型安全的方法來得到了精確的集合類型了。
這種使用類名作為運行時類型標記的技術是一個很有用的技巧,是需要知道的。
在處理注釋的新的API中也有很多類似的情況。
九 通配符的其他作用
(more fun with wildcards,不知道如何譯才比較妥當,呵呵。)
在這部分,我們將會仔細看看通配符的幾個較為深入的用途。我們已經從幾個
有界通配符的例子中看到,它對從某一數據結構中讀取數據是很有用的。現在來看
看相反的情況,只對數據結構進行寫操作。
下面的Sink接口就是這類情況的一個簡單的例子:
interface Sink<T> {
flush(T t);
}
我們可以想象在下面的示范的例子中使用它,writeAll()方法用于把coll集合
里的所有元素填充(flush)到Sink接口變量snk中,并返回最后一個填充的元素。
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);//非法調用
如注釋所注,這里對writeAll()方法的調用是非法的,因為無有效的類型參數
可以引用;String和Object都不適合作為T的類型,因為Collection和Sink的元素
必須是相同類型的。
我們可以通過使用通配符來改寫writeAll()的方法頭來處理,如下:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
String str = writeAll(cs, s);//調用沒問題,但返回類型錯誤
現在調用是合法的了,但由于T的類型跟元素類型是Object的s一樣,因為返回的
類型也是Object,因此賦值是不正確的。
解決辦法是使用我們之前從未見過的一種有界通配符形式:帶下界的通配符。
語法 ? super T 表示了是未知的T的父類型,這與我們之前所使用的有界
(父類型:或者T類型本身,要記住的是,你類型關系是自反的)
通配符是對偶有界通配符,即用 ? extends T 表示未知的T的子類型。
public static<T> T writeAll(Collection<T> coll, Sink<? super T> snk) {...}
...
String str = writeAll(cs, s);//正確!
使用這個語法的調用是合法的,指向的類型是所期望的String類型。
現在我們來看一個比較現實一點的例子,java.util.TreeSet<E>表示元素類型
是E的樹形數據結構里的元素是有序的,創建一個TreeSet對象的一個方法是使用參數
是Comparator對象的構造函數,Comparator對象用于對TreeSet對象里的元素進行
所期望的排序進行分類。
TreeSet(Comparator<E> c)
Comparator接口是必要的:
interface Comparator<T> {
int compare(T fst, T snd);
}
假設我們想要創建一個TreeSet<String>對象,并傳入一下合適的Comparator
對象,我們傳遞的Comparator是能夠比較字符串的。我們可以用Comparator<String>,
但Comparator<Object>也是可以的。但是,我們不能對Comparator<Object>對象
調用上面所給的構造函數,我們可以用一個下界通配符來得到我們想要的靈活性:
TreeSet(Comparator<? super E> c)
這樣就可以使用適合的Comparator對象啦。
最后一個下界通配符的例子,我們來看看Collections.max()方法,這個方法
返回作為參數傳遞的Collection對象中最大的元素。
現在,為了max()方法能正常運行,傳遞的Collection對象中的所有元素都必
須是實現了Comparable接口的,還有就是,它們之間必須是可比較的。
先試一下泛化方法頭的寫法:
public static <T extends Comparable<T>>
T max(Collection<T> coll)
那樣,方法就接受一個自身可比較的(comparable)某個T類型的Collection
對象,并返回T類型的一個元素。這樣顯得太束縛了。
來看看為什么,假設一個類型可以與合意的對象進行比較:
class Foo implements Comparable<Object> {...}
...
Collection<Foo> cf = ...;
Collectins.max(cf);//應該可以正常運行
cf里的每個對象都可以和cf里的任意其他元素進行比較,因為每個元素都是Foo
的對象,而Foo對象可以與任意的對象進行比較,特別是同是Foo對象的。但是,使用
上面的方法頭,我們會發現這樣的調用是不被接受的,指向的類型必須是Foo,但Foo
并沒有實現Comparable<Foo>。
T對于自身的可比性不是必須的,需要的是T與其父類型是可比的,就像下面:
(實際的Collections.max()方法頭在后面的第10部分將會講得更多)
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
這樣推理出來的結果基本上適用于想用Comparable來用于任意類型的用法:
就是你想這樣用Comparable<? super T>。
總的來說,如果你有一個只能一個T類型參數作為實參的API的話,你就應該用
下界通配符類型(? suer T);相反,如果API只返回T對象,你就應該用上界通
配符類型(? extends T),以使得你的客戶的代碼有更大的靈活性。
九-1 通配符捕捉(?wildcard capture)
現在應該很清楚,給出下面的例子:
Set<?> unknownSet = new HashSet<String>();
...
/** 給Set對象s添加一個元素t*/
public static <T> void addToSet<Set<T> s, T t) {...}
下面的調用是非法的。
addToSet(unknownSet, "abc");//非法的
這無異于實際傳遞的Set對象是一個字符串類型的Set對象,問題是作為實參傳遞的
是一個未知類型的Set對象,這樣就不能保證它是一個字符串類型或其他類型的Set對象。
現在,來看下面:
class Collections{
...
<T> public static Set<T> unmodifiableSet<Set<T> set) {...}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet);//這是可以的,
//為什么呢?
看起來這應該是不允許的,但是請看看這個特別的調用,這完全是安全的,因此
這是允許的。這里的unmodifiableSet()確實是對任何類型的Set都適合,不管它的
元素類型是什么。
因為這種情況出現得相對頻繁,因此就有一個特殊的規則,對代碼能夠被檢驗是
安全的任何特定的環境,那樣的代碼都是允許的。這個規則就是所謂的“通配符捕捉”,
允許編譯器對泛型方法引用未知類型的通配符作為類型實參。
十 把遺留代碼轉化為泛型代碼
早前,我們展示了如何使泛型代碼和遺留代碼交互,現在該是時候來看看更難的
問題:把老代碼改為泛型代碼。
如果決定了把老代碼轉換為泛型代碼,你必須慎重考慮如何修改你的API。
你不能對泛型API限制得太死,它得要繼續支持API的最初約定。再看幾個關于
java.util.Collection的例子。非泛型的API就像這樣:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
先這樣簡單來嘗試一下泛化:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
這個當然是類型安全的,它沒有做到API的最初約定,containsAll()方法接受
傳入的任何類型的Collection對象,只有當Collection對象中只包括E類型的實例
的時候才正確。但是:
· 傳入的Collection對象的靜態類型可能不同,這樣的原因可能是調用者不知道
傳入的Collection對象的精確類型,又或者它是Collection<S>類型的,其中S是E的
子類型。
· 對不同類型的Collection對象調用方法containsAll()完全是合法的,程序
應該能夠運行,返回的是false值。
對于addAll()方法這種情況,我們應該能夠添加任何存在了E類型的子類型的Collection
對象,我們在第5部分中看過了如何正確處理這種情況。
還要保證改進的API能夠保留對老客戶的二進制支持(? binary compatibility)。
這就意味著API“擦除”后(erasure)必須與最初的非泛型API一致。在大多數的情
況的結果是自然而然的,但有些小地方卻不盡如此。我們將仔細去看看我們之前遇到
過的最小的Collections.max()方法,正如我們在第9部分所見,似乎正確的max()
的方法頭:
public static <T extends Comparable<? super T>>
T max(CollectionT> coll)
基本沒問題,除了方法頭被“擦除”后的情況:
public static Comparable max(Collection coll)
這與max()方法最初的方法頭不一樣:
public static Object max(Collection coll)
本來是想得到想要的max()方法頭,但是沒成功,所有老的二進制class文件
調用的Collections.max()都依賴于一個返回Object類型的方法頭。
我們可以在類型參數T的邊界中顯式指定一個父類型來強制改變“擦除”的結果。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
這是一個對類型參數給出多個邊界的例子,語法是這樣:T1 & T2 ... & Tn.
多邊界類型變量對邊界類型列表中的所有類型的子類型都是可知的,當使用多邊界
類型的時候,邊界類型列表中的第一個類型將被作為類型變量“擦除”后的類型。
最后,我們應該記住max()方法只是從傳入的Collection方法中讀取數據,
因此也就適用于類型是T的子類型的任何Collection對象。
這樣就有了我們JDK中實際的方法頭:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在實踐中很少會有涉及到這么多東西的情況,但專業類型設計者在轉換現有的API
的時候應該有所準備的仔細思慮。
另一個問題就是要小心協變返回(covariant returns)的情況,那就是改進
子類中方法的返回類型。你不應該在老API中使用這個特性。
假設你最初的API是這樣的:
public class Foo {
public Foo create {...}//工廠方法,應該是創建聲明的類的一個實例
}
public class Bar extends Foo {
public Foo create() {...}//實際是創建一個Bar實例
}
用協變返回的話,是這樣改:
public class Foo {
public Foo create {...}//工廠方法,應該是創建聲明的類的一個實例
}
public class Bar extends Foo {
public Bar create() {...}//實際是創建一個Bar實例
}
現在,假設有這樣的第三方客戶代碼:
public class Baz extends Bar {
public Foo create() {...} //實際是創建一個Baz實例
}
Java虛擬機不直接支持不同返回類型的方法的覆蓋,編譯器就是支持這樣的
特性。結果就是,除非重編譯Baz類,否則的話它不會正確覆蓋Bar中的create()
方法。此外,Baz類需要修改,因為上面寫的代碼不能通過編譯,Baz中create()
方法的返回類型不是Bar類中create()方法的返回類型的子類型。