Java1.5泛型指南中文版<轉(zhuǎn)>
摘要和關(guān)鍵字
1. 介紹
2. 定義簡(jiǎn)單的泛型
3. 泛型和子類繼承
4. 通配符(Wildcards)
4.1. 有限制的通配符(Bounded Wildcards)
5. 泛型方法
6. 與舊代碼交互
6.1. 在泛型代碼中使用老代碼
6.2. 擦除和翻譯(Erasure and Translation)
6.3. 在老代碼中使用泛型代碼
7. 要點(diǎn)(The Fine Print)
7.1. 一個(gè)泛型類被其所有調(diào)用共享
7.2. 轉(zhuǎn)型和instanceof
7.3. 數(shù)組Arrays
8. Class Literals as Run-time Type Tokens
9. More fun with *
9.1. 通配符匹配(wildcard capture)
10. 泛型化老代碼
11. 致謝
1. 介紹
JDK1.5中引入了對(duì)java語(yǔ)言的多種擴(kuò)展,泛型(generics)即其中之一。
這個(gè)教程的目標(biāo)是向您介紹java的泛型(generic)。你可能熟悉其他語(yǔ)言的泛型,最著名的是C++的模板(templates)。如果這樣,你很快就會(huì)看到兩者的相似之處和重要差異。如果你不熟悉相似的語(yǔ)法結(jié)構(gòu),那么更好,你可以從頭開(kāi)始而不需要忘記誤解。
Generics允許對(duì)類型進(jìn)行抽象(abstract over types)。最常見(jiàn)的例子是集合類型(Container types),Collection的類樹(shù)中任意一個(gè)即是。
下面是那種典型用法:
List myIntList = new LinkedList();// 1
myIntList.add(new Integer(0));// 2
Integer x = (Integer) myIntList.iterator().next();// 3
第3行的類型轉(zhuǎn)換有些煩人。通常情況下,程序員知道一個(gè)特定的list里邊放的是什么類型的數(shù)據(jù)。但是,這個(gè)類型轉(zhuǎn)換是必須的(essential)。編譯器只能保證iterator返回的是Object類型。為了保證對(duì)Integer類型變量賦值的類型安全,必須進(jìn)行類型轉(zhuǎn)換。
當(dāng)然,這個(gè)類型轉(zhuǎn)換不僅僅帶來(lái)了混亂,它還可能產(chǎn)生一個(gè)運(yùn)行時(shí)錯(cuò)誤(run time error),因?yàn)槌绦騿T可能會(huì)犯錯(cuò)。
程序員如何才能明確表示他們的意圖,把一個(gè)list中的內(nèi)容限制為一個(gè)特定的數(shù)據(jù)類型呢?這是generics背后的核心思想。這是上面程序片斷的一個(gè)泛型版本:
List<Integer> myIntList = new LinkedList<Integer>(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = myIntList.iterator().next(); // 3
注意變量myIntList的類型聲明。它指定這不是一個(gè)任意的List,而是一個(gè)Integer的List,寫(xiě)作:List<Integer>。我們說(shuō)List是一個(gè)帶一個(gè)類型參數(shù)的泛型接口(a generic interface that takes a type parameter),本例中,類型參數(shù)是Integer。我們?cè)趧?chuàng)建這個(gè)List對(duì)象的時(shí)候也指定了一個(gè)類型參數(shù)。
另一個(gè)需要注意的是第3行沒(méi)了類型轉(zhuǎn)換。
現(xiàn)在,你可能認(rèn)為我們已經(jīng)成功地去掉了程序里的混亂。我們用第1行的類型參數(shù)取代了第3行的類型轉(zhuǎn)換。然而,這里還有個(gè)很大的不同。編譯器現(xiàn)在能夠在編譯時(shí)檢查程序的正確性。當(dāng)我們說(shuō)myIntList被聲明為L(zhǎng)ist<Integer>類型,這告訴我們無(wú)論何時(shí)何地使用myIntList變量,編譯器保證其中的元素的正確的類型。與之相反,一個(gè)類型轉(zhuǎn)換說(shuō)明程序員認(rèn)為在那個(gè)代碼點(diǎn)上它應(yīng)該是那種類型。
實(shí)際結(jié)果是,這可以增加可讀性和穩(wěn)定性(robustness),尤其在大型的程序中。
2. 定義簡(jiǎn)單的泛型
下面是從java.util包中的List接口和Iterator接口的定義中摘錄的片斷:
public interface List<E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
這些都應(yīng)該是很熟悉的,除了尖括號(hào)中的部分,那是接口List和Iterator中的形式類型參數(shù)的聲明(the declarations of the formal type parameters of the interfaces List and Iterator)。
類型參數(shù)在整個(gè)類的聲明中可用,幾乎是所有可是使用其他普通類型的地方(但是有些重要的限制,請(qǐng)參考第7部分)。
(原文:Type parameters can be used throughout the generic declaration, pretty much where you would use ordinary types (though there are some important restrictions; see section 7))
在介紹那一節(jié)我們看到了對(duì)泛型類型聲明List(the generic type declaration List)的調(diào)用,如List<Integer>。在這個(gè)調(diào)用中(通常稱作一個(gè)參數(shù)化類型a parameterized type),所有出現(xiàn)形式類型參數(shù)(formal type parameter,這里是E)都被替換成實(shí)體類型參數(shù)(actual type argument)(這里是Integer)。
你可能想象,List<Integer>代表一個(gè)E被全部替換成Integer的版本:
public interface IntegerList {
void add(Integer x)
Iterator<Integer> iterator();
}
這種直覺(jué)可能有幫助,但是也可能導(dǎo)致誤解。
它有幫助,因?yàn)長(zhǎng)ist<Integer>的聲明確實(shí)有類似這種替換的方法。
它可能導(dǎo)致誤解,因?yàn)榉盒吐暶鹘^不會(huì)實(shí)際的被這樣替換。沒(méi)有代碼的多個(gè)拷貝,源碼中沒(méi)有、二進(jìn)制代碼中也沒(méi)有;磁盤中沒(méi)有,內(nèi)存中也沒(méi)有。如果你是一個(gè)C++程序員,你會(huì)理解這是和C++模板的很大的區(qū)別。
一個(gè)泛型類型的聲明只被編譯一次,并且得到一個(gè)class文件,就像普通的class或者interface的聲明一樣。
類型參數(shù)就跟在方法或構(gòu)造函數(shù)中普通的參數(shù)一樣。就像一個(gè)方法有形式參數(shù)(formal value parameters)來(lái)描述它操作的參數(shù)的種類一樣,一個(gè)泛型聲明也有形式類型參數(shù)(formal type parameters)。當(dāng)一個(gè)方法被調(diào)用,實(shí)參(actual arguments)替換形參,方法體被執(zhí)行。當(dāng)一個(gè)泛型聲明被調(diào)用,實(shí)際類型參數(shù)(actual type arguments)取代形式類型參數(shù)。
一個(gè)命名的習(xí)慣:我們推薦你用簡(jiǎn)練的名字作為形式類型參數(shù)的名字(如果可能,單個(gè)字符)。最好避免小寫(xiě)字母,這使它和其他的普通的形式參數(shù)很容易被區(qū)分開(kāi)來(lái)。許多容器類型使用E作為其中元素的類型,就像上面舉的例子。在后面的例子中還會(huì)有一些其他的命名習(xí)慣。
3. 泛型和子類繼承
讓我們測(cè)試一下我們對(duì)泛型的理解。下面的代碼片斷合法么?
List<String> ls = new ArrayList<String>(); //1
List<Object> lo = ls; //2
第1行當(dāng)然合法,但是這個(gè)問(wèn)題的狡猾之處在于第2行。
這產(chǎn)生一個(gè)問(wèn)題:
一個(gè)String的List是一個(gè)Object的List么?大多數(shù)人的直覺(jué)是回答:“當(dāng)然!”。
好,在看下面的幾行:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 試圖把Object賦值給String
這里,我們使用lo指向ls。我們通過(guò)lo來(lái)訪問(wèn)ls,一個(gè)String的list。我們可以插入任意對(duì)象進(jìn)去。結(jié)果是ls中保存的不再是String。當(dāng)我們?cè)噲D從中取出元素的時(shí)候,會(huì)得到意外的結(jié)果。
java編譯器當(dāng)然會(huì)阻止這種情況的發(fā)生。第2行會(huì)導(dǎo)致一個(gè)編譯錯(cuò)誤。
總之,如果Foo是Bar的一個(gè)子類型(子類或者子接口),而G是某種泛型聲明,那么G<Foo>是G<Bar>的子類型并不成立!!
這可能是你學(xué)習(xí)泛型中最難理解的部分,因?yàn)樗湍愕闹庇X(jué)相反。
這種直覺(jué)的問(wèn)題在于它假定這個(gè)集合不改變。我們的直覺(jué)認(rèn)為這些東西都不可改變。
舉例來(lái)說(shuō),如果一個(gè)交通部(DMV)提供一個(gè)駕駛員里表給人口普查局,這似乎很合理。我們想,一個(gè)List<Driver>是一個(gè)List<Person>,假定Driver是Person的子類型。實(shí)際上,我們傳遞的是一個(gè)駕駛員注冊(cè)的拷貝。然而,人口普查局可能往駕駛員list中加入其他人,這破壞了交通部的記錄。
為了處理這種情況,考慮一些更靈活的泛型類型很有用。到現(xiàn)在為止我們看到的規(guī)則限制比較大。
4. 通配符(Wildcards)
考慮寫(xiě)一個(gè)例程來(lái)打印一個(gè)集合(Collection)中的所有元素。下面是在老的語(yǔ)言中你可能寫(xiě)的代碼:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (int k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
下面是一個(gè)使用泛型的幼稚的嘗試(使用了新的循環(huán)語(yǔ)法):
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
問(wèn)題是新版本的用處比老版本小多了。老版本的代碼可以使用任何類型的collection作為參數(shù),而新版本則只能使用Collection<Object>,我們剛才闡述了,它不是所有類型的collections的父類。
那么什么是各種collections的父類呢?它寫(xiě)作: Collection<?>(發(fā)音為:"collection of unknown"),就是,一個(gè)集合,它的元素類型可以匹配任何類型。顯然,它被稱為通配符。我們可以寫(xiě):
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
現(xiàn)在,我們可以使用任何類型的collection來(lái)調(diào)用它。注意,我們?nèi)匀豢梢宰x取c中的元素,其類型是Object。這永遠(yuǎn)是安全的,因?yàn)椴还躢ollection的真實(shí)類型是什么,它包含的都是objects。但是將任意元素加入到其中不是類型安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 編譯時(shí)錯(cuò)誤
因?yàn)槲覀儾恢纁的元素類型,我們不能向其中添加對(duì)象。
add方法有類型參數(shù)E作為集合的元素類型。我們傳給add的任何參數(shù)都必須是一個(gè)未知類型的子類。因?yàn)槲覀儾恢滥鞘鞘裁搭愋停晕覀儫o(wú)法傳任何東西進(jìn)去。唯一的例外是null,它是所有類型的成員。
另一方面,我們可以調(diào)用get()方法并使用其返回值。返回值是一個(gè)未知的類型,但是我們知道,它總是一個(gè)Object,因此把get的返回值賦值給一個(gè)Object類型的對(duì)象或者放在任何希望是Object類型的地方是安全的。
4.1. 有限制的通配符(Bounded Wildcards)
考慮一個(gè)簡(jiǎn)單的畫(huà)圖程序,它可以用來(lái)畫(huà)各種形狀,比如矩形和圓形。
為了在程序中表示這些形狀,你可以定義下面的類繼承結(jié)構(gòu):
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) { // ...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
// ...
}
}
這些類可以在一個(gè)畫(huà)布(Canvas)上被畫(huà)出來(lái):
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
所有的圖形通常都有很多個(gè)形狀。假定它們用一個(gè)list來(lái)表示,Canvas里有一個(gè)方法來(lái)畫(huà)出所有的形狀會(huì)比較方便:
public void drawAll(List<Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
現(xiàn)在,類型規(guī)則導(dǎo)致drawAll()只能使用Shape的list來(lái)調(diào)用。它不能,比如說(shuō)對(duì)List<Circle>來(lái)調(diào)用。這很不幸,因?yàn)檫@個(gè)方法所作的只是從這個(gè)list讀取shape,因此它應(yīng)該也能對(duì)List<Circle>調(diào)用。我們真正要的是這個(gè)方法能夠接受一個(gè)任意種類的shape:
public void drawAll(List<? extends Shape> shapes) { //..}
這里有一處很小但是很重要的不同:我們把類型 List<Shape> 替換成了 List<? extends Shape>。現(xiàn)在drawAll()可以接受任何Shape的子類的List,所以我們可以對(duì)List<Circle>進(jìn)行調(diào)用。
List<? extends Shape>是有限制通配符的一個(gè)例子。這里?代表一個(gè)未知的類型,就像我們前面看到的通配符一樣。但是,在這里,我們知道這個(gè)未知的類型實(shí)際上是Shape的一個(gè)子類(它可以是Shape本身或者Shape的子類而不必是extends自Shape)。我們說(shuō)Shape是這個(gè)通配符的上限(upper bound)。
像平常一樣,要得到使用通配符的靈活性有些代價(jià)。這個(gè)代價(jià)是,現(xiàn)在像shapes中寫(xiě)入是非法的。比如下面的代碼是不允許的:
public void addRectangle(List<? extends Shape> shapes) {
shapes.add(0, new Rectangle()); // compile-time error!
}
你應(yīng)該能夠指出為什么上面的代碼是不允許的。因?yàn)閟hapes.add的第二個(gè)參數(shù)類型是? extends Shape ——一個(gè)Shape未知的子類。因此我們不知道這個(gè)類型是什么,我們不知道它是不是Rectangle的父類;它可能是也可能不是一個(gè)父類,所以這里傳遞一個(gè)Rectangle不安全。
有限制的通配符正是我們解決DMV給人口普查局傳送名單的例子所需要的。我們的例子假定數(shù)據(jù)用一個(gè)姓名(String)到people(用Person或其子類來(lái)表示,比如Driver)。Map<K,V>是一個(gè)有兩個(gè)類型參數(shù)的泛型類型的例子,表示map的鍵key和值value。
再一次,注意形式類型參數(shù)的命名習(xí)慣——K代表keys,V代表vlaues。
public class Census {
public static void addRegistry(Map<String, ? extends Person> registry) { ...}
}...
Map<String, Driver> allDrivers = ...;
Census.addRegistry(allDrivers);
5. 泛型方法
考慮寫(xiě)一個(gè)方法,它用一個(gè)Object的數(shù)組和一個(gè)collection作為參數(shù),完成把數(shù)組中所有object放入collection中的功能。
下面是第一次嘗試:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // 編譯期錯(cuò)誤
}
}
現(xiàn)在,你應(yīng)該能夠?qū)W會(huì)避免初學(xué)者試圖使用Collection<Object>作為集合參數(shù)類型的錯(cuò)誤了。或許你已經(jīng)意識(shí)到使用 Collection<?>也不能工作。會(huì)議一下,你不能把對(duì)象放進(jìn)一個(gè)未知類型的集合中去。
解決這個(gè)問(wèn)題的辦法是使用generic methods。就像類型聲明,方法的聲明也可以被泛型化——就是說(shuō),帶有一個(gè)或者多個(gè)類型參數(shù)。
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for (T o : a) {
c.add(o); // correct
}
}
我們可以使用任意集合來(lái)調(diào)用這個(gè)方法,只要其元素的類型是數(shù)組的元素類型的父類。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T 指Object
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
fromArrayToCollection(sa, cs);// T inferred to be String
fromArrayToCollection(sa, co);// T inferred to be Object
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
fromArrayToCollection(ia, cn);// T inferred to be Number
fromArrayToCollection(fa, cn);// T inferred to be Number
fromArrayToCollection(na, cn);// T inferred to be Number
fromArrayToCollection(na, co);// T inferred to be Object
fromArrayToCollection(na, cs);// compile-time error
注意,我們并沒(méi)有傳送真實(shí)類型參數(shù)(actual type argument)給一個(gè)泛型方法。編譯器根據(jù)實(shí)參為我們推斷類型參數(shù)的值。它通常推斷出能使調(diào)用類型正確的最明確的類型參數(shù)(原文是:It will generally infer the most specific type argument that will make the call type-correct.)。
現(xiàn)在有一個(gè)問(wèn)題:我們應(yīng)該什么時(shí)候使用泛型方法,又什么時(shí)候使用通配符類型呢?
為了理解答案,讓我們先看看Collection庫(kù)中的幾個(gè)方法。
public interface Collection<E> {
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}
我們也可以使用泛型方法來(lái)代替:
public interface Collection<E> {
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
// hey, type variables can have bounds too!
}
但是,在 containsAll 和 addAll中,類型參數(shù)T 都只使用一次。返回值的類型既不依賴于類型參數(shù)(type parameter)也不依賴于方法的其他參數(shù)(這里,只有簡(jiǎn)單的一個(gè)參數(shù))。這告訴我們類型參數(shù)(type argument)被用作多態(tài)(polymorphism),它唯一的效果是允許在不同的調(diào)用點(diǎn),可以使用多種實(shí)參類型(actual argument)。如果是這種情況,應(yīng)該使用通配符。通配符就是被設(shè)計(jì)用來(lái)支持靈活的子類化的,這是我們?cè)谶@里要強(qiáng)調(diào)的。
泛型函數(shù)允許類型參數(shù)被用來(lái)表示方法的一個(gè)或多個(gè)參數(shù)之間的依賴關(guān)系,或者參數(shù)與其返回值的依賴關(guān)系。如果沒(méi)有這樣的依賴關(guān)系,不應(yīng)該使用泛型方法。
(原文:Generic methods allow type parameters to be used to express dependencies among the types of one or more arguments to a method and/or its return type. If there isn’t such a dependency, a generic method should not be used.)
一前一后的同時(shí)使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src){...}
}
注意兩個(gè)參數(shù)的類型的依賴關(guān)系。任何被從源list從拷貝出來(lái)的對(duì)象必須能夠?qū)⑵渲付槟繕?biāo)list(dest) 的元素的類型——T類型。因此源類型的元素類型可以是T的任意子類型,我們不關(guān)心具體的類型。
copy方法的簽名使用一個(gè)類型參數(shù)表示了類型依賴,但是使用了一個(gè)通配符作為第二個(gè)參數(shù)的元素類型。我們也可以用其他方式寫(xiě)這個(gè)函數(shù)的簽名而根本不使用通配符:
class Collections {
public static <T, S extends T> void copy(List<T> dest, List<S> src){...}
}
這也可以,但是第一個(gè)類型參數(shù)在dst的類型和第二個(gè)參數(shù)的類型參數(shù)S的上限這兩個(gè)地方都有使用,而S本身只使用一次,在src的類型中——沒(méi)有其他的依賴于它。這意味著我們可以用通配符來(lái)代替S。使用通配符比聲明顯式的類型參數(shù)更加清晰和準(zhǔn)確,所以在可能的情況下使用通配符更好。
通配符還有一個(gè)優(yōu)勢(shì)式他們可以在方法簽名之外被使用,比如field的類型,局部變量和數(shù)組。這就有一個(gè)例子。
回到我們的畫(huà)圖問(wèn)題,假定我們想要保持畫(huà)圖請(qǐng)求的歷史記錄。我們可以把歷史記錄保存在Shape類的一個(gè)靜態(tài)成員變量里,在drawAll() 被調(diào)用的時(shí)候把傳進(jìn)來(lái)的參數(shù)保存進(jìn)歷史記錄:
static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
}
最終,再說(shuō)一下類型參數(shù)的命名習(xí)慣。
我們使用T 代表類型,無(wú)論何時(shí)都沒(méi)有比這更具體的類型來(lái)區(qū)分它。這經(jīng)常見(jiàn)于泛型方法。如果有多個(gè)類型參數(shù),我們可能使用字母表中T的臨近的字母,比如S。如果一個(gè)泛型函數(shù)在一個(gè)泛型類里邊出現(xiàn),最好避免在方法的類型參數(shù)和類的類型參數(shù)中使用同樣的名字來(lái)避免混淆。對(duì)內(nèi)部類也是同樣。
6. 與舊代碼交互
直到現(xiàn)在,我們的例子中都假定了一個(gè)理想的世界,那里所有人使用的都是最新版本的java編程語(yǔ)言,它支持泛型。
唉,現(xiàn)實(shí)并非如此。百萬(wàn)行代碼都是在早先版本的語(yǔ)言下寫(xiě)作的,他們不可能一晚上就轉(zhuǎn)換過(guò)來(lái)。
后面,在第10部分,我們會(huì)解決把老代碼轉(zhuǎn)換為使用泛型的代碼的問(wèn)題。在這里,我們把注意力放在一個(gè)更簡(jiǎn)單的問(wèn)題:老代碼怎么和泛型代碼交互?這個(gè)問(wèn)題包括兩部分:在泛型中使用老代碼和在老代碼中使用泛型代碼。
6.1. 在泛型代碼中使用老代碼
怎樣才能使用老代碼的同時(shí)在自己的代碼中享受泛型帶來(lái)的好處?
作為一個(gè)例子,假定你像使用包 com.Fooblibar.widgets。Fooblibar.com(完全虛構(gòu)出來(lái)的公司) 的人們出售一種進(jìn)行庫(kù)存管理的系統(tǒng),下面是主要代碼:
package com.Fooblibar.widgets;
public interface Part { ...}
public class Inventory {
/**
* 添加一個(gè)新配件到庫(kù)存數(shù)據(jù)庫(kù)
* 配件有名字name, 并由零件(Part)的集合組成。
* 零件由parts 指定. collection parts 中的元素必須實(shí)現(xiàn)Part接口。
**/
public static void addAssembly(String name, Collection parts) {...} public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
Collection getParts(); // Returns a collection of Parts
}
現(xiàn)在,你想使用上述API寫(xiě)新代碼。如果能保證調(diào)用addAssembly()時(shí)總是使用正確的參數(shù)會(huì)很棒——就是說(shuō),你傳進(jìn)去的確實(shí)時(shí)一個(gè)Part的Collection。當(dāng)然,泛型可以實(shí)現(xiàn)這個(gè)目的:
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<Part> c = new ArrayList<Part>();
c.add(new Guillotine()) ;
c.add(new Blade());
Inventory.addAssembly(”thingee”, c);
Collection<Part> k = Inventory.getAssembly(”thingee”).getParts();
}
}
當(dāng)我們調(diào)用addAssembly,它希望第二個(gè)參數(shù)是Collection類型。而實(shí)際參數(shù)是Collection<Part> 類型。這可以工作,但是為什么?畢竟,大多數(shù)集合不包含Part對(duì)象,而且總的來(lái)說(shuō),編譯器無(wú)法知道Collection指的是什么類型的集合。
在嚴(yán)格的泛型代碼里,Collection應(yīng)該總是帶著類型參數(shù)。當(dāng)一個(gè)泛型類型,比如Collection被使用而沒(méi)有類型參數(shù)時(shí),它被稱作一個(gè)raw type(自然類型??)。
大多數(shù)人的第一直覺(jué)時(shí)Collection實(shí)際上意味著 Collection<Object>。但是,像我們前面看到的,當(dāng)需要Collection<Object>時(shí)傳遞 Collection<Part>是不安全的。類型Collection表示一個(gè)未知類型元素的集合,就像Collection<?>,這樣說(shuō)更準(zhǔn)確。
但是等一下,那也不正確。考慮getParts()這個(gè)調(diào)用,它返回一個(gè)Collection。然后它被賦值給k,而k是Collection<Part>。如果這個(gè)調(diào)用的結(jié)果是一個(gè)Collection<?>,這個(gè)賦值應(yīng)該是一個(gè)錯(cuò)誤。
事實(shí)上,這個(gè)賦值是合法的,但是它產(chǎn)生一個(gè)未檢查警告(unchecked warning)。這個(gè)警告是必要的,因?yàn)槭聦?shí)是編譯器無(wú)法保證其正確性。我們沒(méi)有辦法檢查getAssembly()中的舊代碼來(lái)保證返回的確實(shí)是一個(gè)Collection<Part>。代碼里使用的類型是Collection,可以合法的向其中加入任何Object。
那么,這應(yīng)該是一個(gè)錯(cuò)誤么?理論上講,Yes,但是實(shí)際上講,如果泛型代碼要調(diào)用舊代碼,那么這必須被允許。這取決于你,程序員,在這種情況下來(lái)滿足你自己。這個(gè)賦值是合法的因?yàn)間etAssembly()的調(diào)用約定中說(shuō)它返回一個(gè)Part的集合,即使這個(gè)類型聲明中沒(méi)有顯示出這一點(diǎn)。
因此,自然類型和通配符類型很像,但是他們的類型檢查不是同樣嚴(yán)格。允許泛型與已經(jīng)存在的老代碼相交互是一個(gè)深思熟慮的決定。
從泛型代碼中調(diào)用老代碼具有先天的危險(xiǎn)性,一旦你把泛型編程和非泛型編程混合起來(lái),泛型系統(tǒng)所提供的所有安全保證都失效。然而,你還是比你根本不用泛型要好。至少你知道你這一端的代碼是穩(wěn)定的。
在非泛型代碼遠(yuǎn)比泛型代碼多的時(shí)候,不可避免會(huì)出現(xiàn)兩者必須混合的情況。
如果你發(fā)現(xiàn)你不得不混合舊代碼和泛型代碼,仔細(xì)注意未檢查警告(unchecked warnings),仔細(xì)考慮你怎樣才能證明出現(xiàn)警告的部分代碼是正確的。
如果你仍然犯了錯(cuò),而導(dǎo)致警告的代碼確實(shí)不是類型安全的,那么會(huì)發(fā)生什么?讓我們看一下這種情形。在這個(gè)過(guò)程中,我們將了解一些編譯器工作的內(nèi)幕。
6.2. 擦除和翻譯(Erasure and Translation)
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // compile-time unchecked warning
return ys.iterator().next();
}
這里,我們用一個(gè)老的普通的list的引用來(lái)指向一個(gè)String的list。我們插入一個(gè)Integer到這個(gè)list中,并且試圖得到一個(gè)String。這是明顯的錯(cuò)誤。如果我們忽略這個(gè)警告并且試圖運(yùn)行以上代碼,它將在我們?cè)噲D使用錯(cuò)誤的類型的地方失敗。在運(yùn)行的時(shí)候,上面的代碼與下面的代碼的行為一樣:
public String loophole(Integer x) {
List ys = new LinkedList();
List xs = ys;
xs.add(x);
return (String) ys.iterator().next(); // run time error
}
當(dāng)我們從list中獲取一個(gè)元素的時(shí)候,并且試圖通過(guò)轉(zhuǎn)換為String而把它當(dāng)作一個(gè)string,我們得到一個(gè) ClassCastException。完全一樣的事情發(fā)生在使用泛型的代碼上。
這樣的原因是,泛型是通過(guò)java編譯器的稱為擦除(erasure)的前端處理來(lái)實(shí)現(xiàn)的。你可以(基本上就是)把它認(rèn)為是一個(gè)從源碼到源碼的轉(zhuǎn)換,它把泛型版本的loophole()轉(zhuǎn)換成非泛型版本。
結(jié)果是,java虛擬機(jī)的類型安全和穩(wěn)定性決不能冒險(xiǎn),即使在又unchecked warning的情況下。
(原文:As a result, the type safety and integrity of the Java virtual machine are never at risk, even in the presence of unchecked warnings.)
基本上,擦除去掉了所有的泛型類型信息。所有在尖括號(hào)之間的類型信息都被扔掉了,因此,比如說(shuō)一個(gè)List<String>類型被轉(zhuǎn)換為L(zhǎng)ist。所有對(duì)類型變量的引用被替換成類型變量的上限(通常是Object)。而且,無(wú)論何時(shí)如果結(jié)果代碼類型不正確,會(huì)插入一個(gè)到合適的類型的轉(zhuǎn)換,就像loophole的最后一行那樣。
擦除的全部的細(xì)節(jié)超出了本文的范圍,但是我們給出的簡(jiǎn)單描述與事實(shí)很接近。知道一點(diǎn)這個(gè)有好處,特別是如果你要作一些復(fù)雜的事,比如把現(xiàn)有API轉(zhuǎn)換成使用泛型的代碼(第10部分)或者僅僅是想理解為什么會(huì)這樣。
6.3. 在老代碼中使用泛型代碼
現(xiàn)在讓我們來(lái)考慮相反的情形。假定Fooblibar.com公司的人決定把他們的代碼轉(zhuǎn)換為使用泛型來(lái)實(shí)現(xiàn),但是他們的一些客戶沒(méi)有轉(zhuǎn)換。現(xiàn)在代碼就像下面:
package com.Fooblibar.widgets;
public interface Part { ...}
public 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(); // Returns 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();
}
}
客戶端代碼是在泛型被引入之前完成的,但是它使用了包c(diǎn)om.Fooblibar.widgets和集合庫(kù),它們都使用了泛型。客戶端代碼中的泛型類的聲明都是使用了自然類型(raw types)。第1行產(chǎn)生一個(gè)unchecked warning,因?yàn)橐粋€(gè)自然的Collection被傳遞到一個(gè)需要Collection<Part>的地方,而編譯器無(wú)法保證Collection就是一個(gè)Collection<Part>。
你還有另一種選擇,你可以使用source 1.4 標(biāo)志來(lái)編譯客戶端代碼,以保證不會(huì)產(chǎn)生警告。但是這種情況下你無(wú)法使用jdk1.5 中的任何新特性。
7. 要點(diǎn)(The Fine Print)
7.1. 一個(gè)泛型類被其所有調(diào)用共享
下面的代碼打印的結(jié)果是什么?
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
或許你會(huì)說(shuō)false,但是那你就錯(cuò)了。它打印出true。因?yàn)樗械姆盒皖愋驮谶\(yùn)行時(shí)有同樣的類(class),而不管他們的實(shí)際類型參數(shù)。
事實(shí)上,泛型之所以為泛型就是因?yàn)樗鼘?duì)所有其可能的類型參數(shù),它有同樣的行為;同樣的類可以被當(dāng)作許多不同的類型。
作為一個(gè)結(jié)果,類的靜態(tài)變量和方法也在所有的實(shí)例間共享。這就是為什么在靜態(tài)方法或靜態(tài)初始化代碼中或者在靜態(tài)變量的聲明和初始化時(shí)使用類型參數(shù)申明是不合法的原因。
(原文:As consequence, the static variables and methods of a class are also shared among all the instances. That is why it is illegal to refer to the type parameters of a type declaration in a static method or initializer, or in the declaration or initializer of a static variable.)
7.2. 轉(zhuǎn)型和instanceof
泛型類被所有其實(shí)例(instances)共享的另一個(gè)暗示是檢查一個(gè)實(shí)例是不是一個(gè)特定類型的泛型類是沒(méi)有意義的。
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // 非法
類似的,如下的類型轉(zhuǎn)換
Collection<String> cstr = (Collection<String>) cs;
得到一個(gè)unchecked warning,因?yàn)檫\(yùn)行時(shí)環(huán)境不會(huì)為你作這樣的檢查。
對(duì)類型變量也是一樣:
<T> T badCast(T t, Object o) {
return (T) o; // unchecked warning
}
類型參數(shù)在運(yùn)行時(shí)并不存在。這意味著它們不會(huì)添加任何的時(shí)間或者空間上的負(fù)擔(dān),這很好。不幸的是,這也意味著你不能依靠他們進(jìn)行類型轉(zhuǎn)換。
7.3. 數(shù)組Arrays
數(shù)組對(duì)象的組成類型不能是一個(gè)類型變量或者類型參數(shù),除非它是無(wú)上限的通配符類型。你可以聲明元素類型是一個(gè)類型參數(shù)或者參數(shù)化類型的數(shù)組類型,但不是數(shù)組對(duì)象(譯注:得不到對(duì)象,只能聲明)。
(原文:The component type of an array object may not be a type variable or a parameterized type, unless it is an (unbounded) wildcard type.You can declare array types whose element type is a type variable or a parameterized type, but not array objects.)
這很煩人,但是確實(shí)時(shí)這樣。為了避免下面的情況,必須有這樣的限制:
List<String>[] lsa = new List<String>[10]; // not really allowed
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // unsound, but passes run time store check
String s = lsa[1].get(0); // run-time error - ClassCastException
如果參數(shù)化類型可以是數(shù)組,那么意味著上面的例子可以沒(méi)有任何unchecked warnings的通過(guò)編譯,但是在運(yùn)行時(shí)失敗。我們把類型安全(type-safety)作為泛型首要的設(shè)計(jì)目標(biāo)。特別的,java語(yǔ)言被設(shè)計(jì)為保證:如果你的整個(gè)程序沒(méi)有unchecked warnings的使用javac –source1.5通過(guò)編譯,那么它是類型安全的(原文: if your entire application has been compiled without unchecked warnings using javac -source 1.5, it is type safe)。
然和,你仍然可以使用通配符數(shù)組。上面的代碼有兩種變化。第一種改變放棄使用數(shù)組對(duì)象和元素類型參數(shù)化的數(shù)組類型。結(jié)果是,我們不得不顯式的進(jìn)行類型轉(zhuǎn)換來(lái)從數(shù)組中獲得一個(gè)String。
List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // correct
String s = (String) lsa[1].get(0); // run time error, but cast is explicit
在下面的變體中,我們避免了產(chǎn)生一個(gè)元素類型是參數(shù)化的數(shù)組對(duì)象,但是使用了元素類型參數(shù)化的類型。(譯注:意思如下面的第一行代碼所示,聲明一個(gè)泛型化的數(shù)組,但是new的時(shí)候使用的是raw type,原文中是 new ArrayList<?>(10),那是錯(cuò)的,已經(jīng)修正為new ArrayList(10);)這是合法的,但是產(chǎn)生一個(gè)unchecked warning。實(shí)際上,這個(gè)代碼是不安全的,最后產(chǎn)生一個(gè)錯(cuò)誤。
List<String>[] lsa = new ArrayList[10]; // unchecked warning - this is unsafe!
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // correct
String s = lsa[1].get(0); // run time error, but we were warned
類似的,創(chuàng)建一個(gè)元素類型是一個(gè)類型變量的數(shù)組對(duì)象導(dǎo)致一個(gè)編譯時(shí)錯(cuò)誤:
<T> T[] makeArray(T t) {
return new T[100]; // error
}
因?yàn)轭愋妥兞吭谶\(yùn)行時(shí)并不存在,所以沒(méi)有辦法決定實(shí)際類型是什么。
解決這些限制的辦法是使用字面的類作為運(yùn)行時(shí)類型標(biāo)志(原文:use class literals as run time type tokens),見(jiàn)第8部分。
8. Class Literals as Run-time Type Tokens
JDK1.5中一個(gè)變化是類 java.lang.Class是泛型化的。這是把泛型作為容器類之外的一個(gè)很有意思的例子(using genericity for something other than a container class)。
現(xiàn)在,Class有一個(gè)類型參數(shù)T, 你很可能會(huì)問(wèn),T 代表什么?
它代表Class對(duì)象代表的類型。比如說(shuō),String.class類型代表 Class<String>,Serializable.class代表 Class<Serializable>。著可以被用來(lái)提高你的反射代碼的類型安全。
特別的,因?yàn)?Class的 newInstance() 方法現(xiàn)在返回一個(gè)T, 你可以在使用反射創(chuàng)建對(duì)象時(shí)得到更精確的類型。
比如說(shuō),假定你要寫(xiě)一個(gè)工具方法來(lái)進(jìn)行一個(gè)數(shù)據(jù)庫(kù)查詢,給定一個(gè)SQL語(yǔ)句,并返回一個(gè)數(shù)據(jù)庫(kù)中符合查詢條件的對(duì)象集合(collection)。
一個(gè)方法時(shí)顯式的傳遞一個(gè)工廠對(duì)象,像下面的代碼:
interface Factory<T> {
public T[] make();
}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* run sql query using jdbc */
for (int i=0;i<10;i++/* iterate over jdbc results */ ) {
T item = factory.make();
/* use reflection and set all of item’s fields from sql results */
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");
這個(gè)解決方案的缺點(diǎn)是它需要下面的二者之一:
l 調(diào)用處那冗長(zhǎng)的匿名工廠類,或
l 為每個(gè)要使用的類型聲明一個(gè)工廠類并傳遞其對(duì)象給調(diào)用的地方
這很不自然。
使用class literal作為工廠對(duì)象是非常自然的,它可以被發(fā)射使用。沒(méi)有泛型的代碼可能是:
Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”); ...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/* run sql query using jdbc */
for ( /* iterate over jdbc results */ ) {
Object item = c.newInstance();
/* use reflection and set all of item’s fields from sql results */
result.add(item);
}
return result;
}
但是這不能給我們返回一個(gè)我們要的精確類型的集合。現(xiàn)在Class是泛型的,我們可以寫(xiě):
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>();
/* run sql query using jdbc */
for ( /* iterate over jdbc results */ ) {
T item = c.newInstance();
/* use reflection and set all of item’s fields from sql results */
result.add(item);
}
return result;
}
來(lái)通過(guò)一種類型安全的方式得到我們要的集合。
這項(xiàng)技術(shù)是一個(gè)非常有用的技巧,它已成為一個(gè)在處理注釋(annotations)的新API中被廣泛使用的習(xí)慣用法。
9. More fun with *
在這一部分,我們來(lái)考慮一些通配符得高級(jí)用法。我們已經(jīng)看到了上限通配符在從一個(gè)數(shù)據(jù)結(jié)構(gòu)中進(jìn)行讀取的幾個(gè)例子。現(xiàn)在考慮相反的情況,一個(gè)只寫(xiě)的數(shù)據(jù)結(jié)構(gòu)。
接口Sink是這種情況的一個(gè)簡(jiǎn)單例子。
interface Sink<T> {
void flush(T t);
}
我們可以想象他被如下面的代碼一樣使用。方法writeAll() 被設(shè)計(jì)來(lái)把集合coll的所有元素flush到sink snk,并且返回最后一個(gè)flush的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
T last = null;
for (T t : coll) {
last = t;
snk.flush(last);
}
return last;
}
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // 非法的調(diào)用!!
像上面所寫(xiě),writeAll() 的調(diào)用是非法的,因?yàn)闆](méi)有有效的類型參數(shù)可以被推斷出來(lái)。String 或 Object都不是T的合適的類型,因?yàn)镃ollection的元素和 Sink的元素必須是同樣的類型。
我們可以解決這個(gè)問(wèn)題,通過(guò)使用通配符來(lái)修改writeAll()的方法簽名,如下:
<T> T writeAll(Collection<? extends T> coll, Sink<T> snk) { … }
String str = writeAll(cs, s); //可以調(diào)用但是返回值類型錯(cuò)誤
這個(gè)調(diào)用現(xiàn)在是合法的,但是賦值產(chǎn)生錯(cuò)誤,因?yàn)橥茢喑龅姆祷刂殿愋褪?Object因?yàn)門 匹配了Sink的類型,Object。
解決方案是使用一種我們還沒(méi)有見(jiàn)過(guò)的有限制的通配符:有下限的通配符。語(yǔ)法 ? super T 表示T的一個(gè)未知的父類(或者是T自己)。這跟我們用? extends T 表示T的一個(gè)未知的子類是對(duì)應(yīng)的。
<T> T writeAll(Collection<T> coll, Sink<? super T> snk) { … }
String str = writeAll(cs, s); // YES!!!
使用這個(gè)語(yǔ)法,這個(gè)調(diào)用是合法的,推斷出來(lái)的T是String,正是我們想要的。
現(xiàn)在讓我們看一個(gè)更現(xiàn)實(shí)的例子。一個(gè) java.util.TreeSet<E> 代表一個(gè)有序的元素是E類型的樹(shù)。創(chuàng)建一個(gè)TreeSet的一個(gè)方法是傳遞一個(gè) Comparator 對(duì)象給構(gòu)造函數(shù)。這個(gè)Comparator將會(huì)用來(lái)按照需要對(duì)TreeSet進(jìn)行排序。
TreeSet(Comparator<E> c)
Comparator 接口是核心:
interface Comparator<T> { int compare(T fst, T snd); }
假定我們要?jiǎng)?chuàng)建一個(gè) TreeSet<String> 并傳遞一個(gè)合適的 Comparator,我們需要傳一個(gè)能比較String的Comparator。這可以是一個(gè) Comparator<String>,也可以是一個(gè) Comparator<Object>。然而我們不能用Comparator<Object>來(lái)調(diào)用上面的構(gòu)造函數(shù)。我們可以使用一個(gè)有下限的通配符來(lái)得到我們需要的靈活性:
TreeSet(Comparator<? super E> c)
這允許任何可用的Comparator被傳遞進(jìn)去。
作為使用下限通配符最終的例子,讓我們來(lái)看看方法 Collections.max(),它返回一個(gè)集合中的最大的元素。
現(xiàn)在,為了讓max()能工作,傳進(jìn)來(lái)的集合中的所有元素必須實(shí)現(xiàn) Comparatable接口。而且,他們必須都能夠被彼此比較(all be comparable to each other)。第一個(gè)嘗試是:
public static <T extends Comparable<T>> T max(Collection<T> coll)
就是說(shuō),方法的參數(shù)是某一個(gè)能和自己進(jìn)行比較的T的集合。這限制太嚴(yán)格了。
為什么?考慮一個(gè)能和任何對(duì)象進(jìn)行比較的類型:
class Foo implements Comparable<Object> {...} ...
Collection<Foo> cf = ...;
Collections.max(cf); // 應(yīng)該能工作
cf 中的每個(gè)元素都可以和每個(gè)cf中的其他元素進(jìn)行比較,因?yàn)槊總€(gè)這樣的元素都是一個(gè)Foo,它可以和任意的對(duì)象進(jìn)行比較,也可以和另一個(gè)Foo進(jìn)行比較。
但是,使用上面的方法簽名,我們發(fā)現(xiàn)這個(gè)調(diào)用被拒絕。推斷出來(lái)的類型必須是Foo,但是Foo沒(méi)有實(shí)現(xiàn)接口 Comparable<Foo>。
T 精確的(exactly)和自己能比較是不需要的。所需要的是 T能夠和它的父類中的一個(gè)進(jìn)行比較,這導(dǎo)出:(注:Collections.max()的實(shí)際方法簽名更復(fù)雜,我們?cè)诘?0部分再討論。)
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
這個(gè)推論對(duì)大多數(shù)想讓 Comparable 對(duì)任意類型生效的用法中都有效:你總是應(yīng)該使用 Comparable<? super T>。
總之,如果你有一個(gè)只使用類型參數(shù)T作為參數(shù)的API,它的使用應(yīng)該利用下限通配符( ? super T )的好處。相反的,如果API只返回T,你應(yīng)該使用上限通配符( ? extends T )來(lái)給你的客戶端更大的靈活性。
(原文:This reasoning applies to almost any usage of Comparable that is intended to work for arbitrary types: You always want to use Comparable<? super T>.
In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T). )。
9.1. 通配符匹配(wildcard capture)
現(xiàn)在應(yīng)該很清晰,如果給定:
Set<?> unknownSet = new HashSet<String>(); ...
/** 向 Set s 中添加一個(gè)元素*/
public static <T> void addToSet(Set<T> s, T t) {...}
這個(gè)調(diào)用是非法的:
addToSet(unknownSet, "abc"); // 非法
實(shí)際的set是一個(gè)String的set并不起作用,起作用的是傳進(jìn)來(lái)的表達(dá)式是一個(gè)unknown type的set,它不能保證是一個(gè)String的set或者任何其他的特定類型。
現(xiàn)在,考慮:
class Collections { ...
<T> public static Set<T> unmodifiableSet(Set<T> set) { ... }
}...
Set<?> s = Collections.unmodifiableSet(unknownSet); // this works! Why?
似乎這應(yīng)該不被允許,但是,研究這個(gè)特定的調(diào)用,允許它是非常安全的。畢竟,unmodifiableSet 確實(shí)對(duì)任何種類的Set能工作,不管它的元素類型。
因?yàn)檫@種情況相對(duì)出現(xiàn)的次數(shù)比較多,有一個(gè)特殊的規(guī)則在能證明代碼是安全的情況下允許這樣的代碼。()這個(gè)規(guī)則,稱為wildcard capture,允許編譯器推斷出通配符為unknown type作為一個(gè)泛型方法的類型參數(shù)。
(原文:Because this situation arises relatively frequently, there is a special rule that allows such code under very specific circumstances in which the code can be proven to be safe. This rule, known as wildcard capture, allows the compiler to infer the unknown type of a wildcard as a type argument to a generic method.)
10. 泛型化老代碼
前面,我們講述了新老代碼如何交互。現(xiàn)在,是時(shí)候研究更難的泛型化老代碼的問(wèn)題了。
如果你決定把老代碼轉(zhuǎn)換成使用泛型的代碼,你需要仔細(xì)考慮怎么修改你的API。
你必須確定泛型化的API不會(huì)過(guò)分嚴(yán)格,它必須繼續(xù)支持原來(lái)的API調(diào)用契約(original contract of the API)。在考慮幾個(gè) java.util.Collection中的例子。泛型代碼之前的API像:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
一個(gè)稚嫩的泛型化嘗試:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
這當(dāng)然是類型安全的,但是它不支持這個(gè)API的原始契約(original contract)。
containsAll() 方法能對(duì)所有進(jìn)來(lái)的任意類型的collection工作。它只有在傳進(jìn)來(lái)的collection中真正只包含E的實(shí)例才成功,但是:
l 傳進(jìn)來(lái)的collection的靜態(tài)類型可能不同,可能是因?yàn)檎{(diào)用者不知道傳進(jìn)來(lái)的colleciton的精確類型,或者因?yàn)樗且粋€(gè)Collection<S>,S是E的子類型。
l 用一個(gè)不同類型的collection來(lái)調(diào)用containsAll()應(yīng)該是合法的。這個(gè)例程應(yīng)該能夠工作,返回false。
對(duì)addAll(),我們應(yīng)該能夠添加任何元素是E的子類型的collection。我們已經(jīng)在第5部分講述了怎么正確的處理這種情況。
你還應(yīng)該保證修訂過(guò)的API保持與老客戶端的二進(jìn)制兼容。者以為者API的erasure必須與老的未泛型化版本一樣。在大多數(shù)情況下,這是很自然的結(jié)果,但是有些精巧的情形(subtle cases)。我們看看我們已經(jīng)碰到過(guò)的精巧的情形中的一個(gè)(one of the subtle cases),方法Collections.max()。就像我們?cè)诘?部分看到的,一個(gè)似是而非的max()的方法簽名是:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
這很好,除了擦除(erasure)后的簽名是:
public static Comparable max(Collection coll)
這和老版本的max() 的簽名不同:
public static Object max(Collection coll)
當(dāng)然可以把max()定義為這個(gè)簽名,但是這沒(méi)有成為現(xiàn)實(shí),因?yàn)樗姓{(diào)用了Collections.max()的老的二進(jìn)制class文件依賴于返回Object的簽名。
我們可以強(qiáng)迫the erasure不同,通過(guò)給形式類型參數(shù)T顯式的定義一個(gè)父類。
public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)
這是一個(gè)對(duì)一個(gè)類型參數(shù)給定多個(gè)界限(multiple bounds)的例子,是用語(yǔ)法 T1 & T2 … & Tn。一個(gè)有多個(gè)界限的類型的參數(shù)是所有界限中列出來(lái)的類型的子類。當(dāng)多個(gè)界限被使用的時(shí)候,界限中的第一個(gè)類型被用作這個(gè)類型參數(shù)的erasure。
(原文:This is an example of giving multiple bounds for a type parameter, using the syntax T1& T2 ... & Tn. A type variable with multiple bounds is known to be a subtype of all of the types listed in the bound. When a multiple bound is used, the first type mentioned in the bound is used as the erasure of the type variable.)
最后,我們應(yīng)該想到max只從傳進(jìn)來(lái)的collection中讀取數(shù)據(jù),因此它對(duì)元素是T的子類的collection可用。這給我們JDK中使用的真正的簽名:
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
實(shí)際中出現(xiàn)那么棘手的問(wèn)題是很罕見(jiàn)的,但是專業(yè)庫(kù)設(shè)計(jì)師應(yīng)該準(zhǔn)備好非常仔細(xì)的考慮轉(zhuǎn)換現(xiàn)存的API。
另一個(gè)需要小心的問(wèn)題是協(xié)變式返回值(covariant returns),就是說(shuō)在子類中獲得一個(gè)方法的返回值(refining the return type of a method in a subclass)。在老API中你無(wú)法使用這個(gè)特性帶來(lái)的好處。
為了知其原因,讓我們看一個(gè)例子。
假定你的原來(lái)的API是下面的形式:
public class Foo {
public Foo create(){...}
// Factory, should create an instance of whatever class it is declared in
}
public class Bar extends Foo {
public Foo create(){...} // actually creates a Bar
}
為了使用協(xié)變式返回值的好處,你把它改成:
public class Foo {
public Foo create(){...}
// Factory, should create an instance of whatever class it is declared in
}
public class Bar extends Foo {
public Bar create(){...} // actually creates a Bar
}
現(xiàn)在,假定你的一個(gè)第三方客戶代碼:
public class Baz extends Bar {
public Foo create(){...} // actually creates a Baz
}
Java虛擬機(jī)并不直接支持不同類型返回值的方法重載。這個(gè)特性是由編譯器來(lái)支持的。因此,除非Baz類被重新編譯,它不會(huì)正確的重載Bar的create()方法,而且,Baz必須被修改,因?yàn)锽az的代碼被拒絕,它的create的返回值不是Bar中create返回值的子類。(原文: Consequently, unless the class Baz is recompiled, it will not properly override the create() method of Bar.Furthermore, Baz will have to be modified, since the code will be rejected as written - the return type of create() in Baz is not a subtype of the return type of create() in Bar.)
(譯注:上面的一段話有些莫名其妙,我測(cè)試過(guò)這個(gè)例子,在jdk1.4下,三個(gè)類都編譯之后改變Bar,只在jdk5下重新編譯Bar,然后在jdk5下,Baz仍然能夠被使用,當(dāng)然那,無(wú)法使用 Baz b = baz.create();這樣的代碼。)
11. 致謝
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.
-------------------------------------------------------------------------------------------------
PS:本博客文章,如果沒(méi)有注明是有“轉(zhuǎn)”字樣,屬于本人原創(chuàng)。如果需要轉(zhuǎn)載,務(wù)必注明作者和文章的詳細(xì)出處地址,否則不允許轉(zhuǎn)載,多謝合作!
posted on 2007-10-21 19:25
apple0668 閱讀(2508)
評(píng)論(3) 編輯 收藏 所屬分類:
java