Java Tutorials -- Generics
Java Generics伴隨JDK 5.0發(fā)布到現(xiàn)在已經(jīng)超過(guò)2年半了,但目前還沒(méi)有被"非常廣泛"地應(yīng)用,我也一直沒(méi)有進(jìn)行過(guò)系統(tǒng)的學(xué)習(xí)。最近使用Thinking in Java(4th)和Java Tutorials對(duì)泛型進(jìn)行了專門的學(xué)習(xí)。本文是對(duì)Java Tutorials中Generics一章的翻譯。其實(shí)關(guān)于Java Generics的文章已是汗牛充棟,之所以將這篇譯文放在此處,也算是對(duì)自己學(xué)習(xí)的一種鼓勵(lì)吧。該文的讀者應(yīng)該只有我一人,但仍然希望對(duì)其他朋友有所助益。(2007.07.10最后更新)1 介紹
JDK 5.0引進(jìn)了幾種Java程序設(shè)計(jì)語(yǔ)言的新擴(kuò)展。其中之一,就是對(duì)泛型的引入。
本次體驗(yàn)只是對(duì)泛型的介紹。你可能通過(guò)其它的語(yǔ)言,特別是C++ Template,已經(jīng)對(duì)泛型的結(jié)構(gòu)有些熟悉了。如果是這樣的話,你將看到它們的相似點(diǎn)和重要的不同點(diǎn)。如果你對(duì)從別處看到的這種似曾相識(shí)的結(jié)構(gòu)不熟悉的話,那就更好了,你可以從頭開(kāi)始,以避免不得不忘卻一些誤解。
泛型允許你抽象出類型。最普通的例子就是容器類型,如集合框架(Collection)中的那些類。
下面是一個(gè)此類特性的典型使用:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
第三行的強(qiáng)制類型轉(zhuǎn)換有點(diǎn)煩人。基本上,程序員知道到底是什么類型的數(shù)據(jù)被放到這個(gè)特定的List中了。然而,這個(gè)強(qiáng)制類型轉(zhuǎn)換是必需的。編譯器只能保證迭代器將返回的是一個(gè)對(duì)象。為了確保一個(gè)類型為Integer的變量x是類型安全的,這個(gè)強(qiáng)制類型轉(zhuǎn)換是需要的。
當(dāng)然,這個(gè)強(qiáng)制類型轉(zhuǎn)換并不會(huì)造成混亂。它仍然可能會(huì)造成一個(gè)運(yùn)行時(shí)錯(cuò)誤,可能是由程序員的失誤而產(chǎn)生的。
那么程序員如何才能準(zhǔn)確地表達(dá)他們的本意,使得一個(gè)List被限制為只能包含某個(gè)特定類型的數(shù)據(jù)呢?這正是泛型背后的核心思想。下面的程序片斷是前述例子的泛型版:
List<Integer> myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
注意變量myIntList的類型聲明。它不是指定了一個(gè)任意的List,而是指定了一個(gè)Integer對(duì)象的List,寫作List<Integer>。我們說(shuō),List是一個(gè)擁有類型參數(shù),在此處就是Integer,的泛型接口。當(dāng)創(chuàng)建這個(gè)List對(duì)象時(shí),我們也指定了一個(gè)類型參數(shù)。
再次注意,原來(lái)行3的的強(qiáng)制類型轉(zhuǎn)換已經(jīng)不需要了。
現(xiàn)在你可能會(huì)想我們所已經(jīng)完成的就是移除了那個(gè)混亂(強(qiáng)制類型轉(zhuǎn)換)。我們?cè)谛?處就使Integer成為一個(gè)類型參數(shù),而不是在行3處進(jìn)行強(qiáng)制類型轉(zhuǎn)換。這兒就有一個(gè)很大的不同。在編譯時(shí),編譯器就能夠檢查程序中的類型是否正確。當(dāng)我們說(shuō)myIntList在聲明時(shí)使用了類型List<Integer>,那么就是告訴我們myIntList變量在任何時(shí)間和任何地點(diǎn)所包含的類型必須是Integer,并且編譯器會(huì)確保這一點(diǎn)。相反地,強(qiáng)制類型轉(zhuǎn)換只是告訴我們?cè)诖a中的某個(gè)獨(dú)立的地方程序員所期望的情況而以。
在實(shí)際情況下,特別是在大型應(yīng)用中,泛型可以提高程序的可讀性和魯棒性。
2 定義簡(jiǎn)單的泛型
下面是java.util包中List和Iterator接口定義的簡(jiǎn)短摘要:
public interface List <E>{
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E>{
E next();
boolean hasNext();
}
除了角括號(hào)中的內(nèi)容,我們對(duì)這段代碼應(yīng)該比較熟悉了。這些是List和Iterator接口的形式類型參數(shù)的聲明。
類型參數(shù)的使用可以貫穿于整個(gè)泛型聲明,用在那些你以后想使用普通類型的地方(但有一些重要的約束,詳見(jiàn)"良好的打印"一節(jié))。
在"介紹"一節(jié)中,我們知道了使用了泛型類型聲明的List接口的調(diào)用方法,如List<Integer>。在這個(gè)調(diào)用(一般就是調(diào)用一個(gè)參數(shù)化的類型)中,所有形式類型參數(shù)(即此處的E)出現(xiàn)的地方都被實(shí)際的類型參數(shù)(即此處的Integer)替換了。
你可能會(huì)想像List<Integer>表示一種由Integer統(tǒng)一地代替E之后的新的List版本:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
這種直覺(jué)是有助益的,但那也是誤解。
說(shuō)它是有助益的,是因?yàn)閰?shù)類型List<Integer>實(shí)際上所使用的方法看起來(lái)就是像那種擴(kuò)展。
說(shuō)它是誤解,是因?yàn)榉盒偷穆暶鞔_實(shí)沒(méi)有用那種方式進(jìn)行擴(kuò)展。并不存在那些代碼的多個(gè)復(fù)本,在源文件中、二進(jìn)制文件中、硬盤中、內(nèi)存中都沒(méi)有這些復(fù)本。如果你是C++程序員,你將會(huì)發(fā)現(xiàn)這與C++ Template非常的不同。
泛型類型的聲明絕對(duì)只會(huì)被編譯一次,然后進(jìn)入一個(gè)class文件中,就像一個(gè)普通的類或接口聲明一樣。
類型參數(shù)類似于方法或構(gòu)造器中的普通參數(shù)。它非常像一個(gè)方法擁有一個(gè)形式值參數(shù),這個(gè)參數(shù)描述了可以出現(xiàn)在該處的值的類型,泛型聲明也有一個(gè)形式類型參數(shù)。當(dāng)一個(gè)方法被調(diào)用時(shí),一個(gè)真實(shí)的的參數(shù)會(huì)替換形式參數(shù),然后這個(gè)方法會(huì)進(jìn)行評(píng)估。當(dāng)一個(gè)泛型聲明被調(diào)用時(shí),一個(gè)真實(shí)的類型參數(shù)也會(huì)替代形式類型參數(shù)。
需要注重一個(gè)命名規(guī)范。我們推薦你使用叫起來(lái)盡量精簡(jiǎn)的(如果可能的話,最好是單個(gè)字母)的名字作為形式類型參數(shù)。最好避免使用小寫字母,這樣就可以很容易地從普通的類和接口中區(qū)分出形式類型參數(shù)。如上述例子中,很多容器類型使用E代表容器中的元素(element)。
3 泛型與子類
讓我們測(cè)試一下你對(duì)泛型的理解。下面的代碼片斷是合法的嗎?
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第一行肯定是合法的。這個(gè)問(wèn)題狡猾的部分是在第二行。這個(gè)問(wèn)題可歸結(jié)為:一個(gè)String對(duì)象的List也是Object對(duì)象的List嗎?大部分人都會(huì)本能的回答到,是的!
那好,來(lái)看看下面幾行:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 試圖將一個(gè)Object對(duì)象賦值給一個(gè)String變量!
此處我們已經(jīng)別名化了ls和lo。通過(guò)別名lo訪問(wèn)ls,一個(gè)String對(duì)象的List,我們可以向其中插入任意對(duì)象。但ls不能包含除String對(duì)象外的其它對(duì)象,則當(dāng)我們?cè)噲D從中獲得些什么(Object對(duì)象)時(shí),我們會(huì)感到非常的驚訝。
譯者:上面這段話的意思是說(shuō),如果上述4行代碼都成立的話,那么就會(huì)使我們感到很驚訝、很困惑。lo的類型是List<Object>,那么可以放入任意的Object到這個(gè)List中;而ls的類型是List<String>,即只能放入String對(duì)象。但lo引用的對(duì)象實(shí)際上是ArrayList<String>的對(duì)象,即只能存放String對(duì)象,所以上面的例子會(huì)使人感到很困惑。
當(dāng)然,Java編譯器會(huì)阻止這一切的發(fā)生--第二行將會(huì)導(dǎo)致一個(gè)編譯時(shí)錯(cuò)誤。
一般地,如果Foo是Bar的子類型(子類或子接口),且G是某個(gè)泛型類型聲明,那么G<Foo>并不是G<Bar>的子類型。這可能是當(dāng)你學(xué)習(xí)泛型時(shí)所遇到的最困難的問(wèn)題,因?yàn)檫@違反了我們根深蒂固的直覺(jué)。
我們不能假設(shè)集成對(duì)象們不會(huì)改變。我們的直覺(jué)可能會(huì)導(dǎo)致我們靜態(tài)地思考這些問(wèn)題。
例如,如果機(jī)動(dòng)車管理部(Department of Motor Vehicles, DMV)向人口調(diào)查局(Census Bureau)提交了一組司機(jī)的名單,這會(huì)被看成是合理的,因?yàn)槲覀冋J(rèn)為L(zhǎng)ist<Driver>是List<Person>的子類型(假設(shè)Driver是Person的子類型)。實(shí)際上被提交的只是司機(jī)注冊(cè)表的副本。否則,人口調(diào)查局也可以把那些不是司機(jī)的人也加入到這個(gè)名單 (List)中,這就會(huì)破壞DMV的記錄。
為了應(yīng)對(duì)這種情況,有必要考慮更為彈性的泛型類型。我們到目前為止所看到的規(guī)則實(shí)在是太具限制性了。
4 通配符
考慮這樣一個(gè)問(wèn)題,寫一個(gè)程序打印出一個(gè)集合對(duì)象中的所有元素。下面的程序可能是你用老版Java語(yǔ)言所寫的:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
這兒有一個(gè)不成熟的對(duì)泛型應(yīng)用的嘗試(并且使用了新的foreach循環(huán)語(yǔ)法):
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
這個(gè)問(wèn)題就是新版的程序并不比舊版的程序更有用。反之,舊版的程序能夠作為參數(shù)被任何類型的集合對(duì)象調(diào)用,新版的程序只能用于Collection<Object>,而這種情況已經(jīng)被我們證明了,它并不是所有集合類型的超類。
那么什么才是所有集合對(duì)象的超類呢?它應(yīng)該寫作Collection<?>(叫作"collection of unknow,未知的集合"),這種集合類型的元素才可能配置任何類型。很明顯,它被稱作通配符類型。我們可以這樣寫:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
然后我們就可以用任何集合類型來(lái)調(diào)用這個(gè)方法了。注意printCollection方法的內(nèi)部,我們?nèi)匀豢梢詮腸中讀取它的元素,并可將這些元素賦值給Object類型的變量。
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error
由于不知道c中元素的類型是什么,我們不能向它里面添加元素。add方法接受類型E的參數(shù),即這個(gè)集合對(duì)象元素的類型。當(dāng)然實(shí)際的類型參數(shù)是"?"時(shí),它表示某個(gè)未知的類型。任何我們要添加入的參數(shù)都將不得不是未知類型的子類型。由于我們不知道這個(gè)類型是什么,所以我們不能傳入任何類型。唯一的例外是 "null",null是每個(gè)類型的成員(譯者:null是每種類型的子類型。)。
另一方面,給出一個(gè)List<?>,我們就能調(diào)用get方法并使用得到的結(jié)果。所得結(jié)果的類型是未知的,但我們總可以知道它是一個(gè) Object對(duì)象。因此將由get方法得到的結(jié)果賦予一個(gè)Object類型的變量,或是將它作為一個(gè)參數(shù)傳入一個(gè)期望獲得Object類型對(duì)象的地方,都是完全的。
有邊界的通配符
考慮這樣的一個(gè)簡(jiǎn)單的繪圖程序,它可以繪制諸如矩形和環(huán)形之類的形狀。為了使用程序來(lái)描述這些形狀,你可能是會(huì)下面那樣定義一組類:
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)上:
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
任何繪制動(dòng)作通常都會(huì)包含一組形狀。假設(shè)使用List來(lái)表示它們,那么為方便起見(jiàn),Canvas需要有一個(gè)方法去繪制所有的形狀:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
現(xiàn)在,規(guī)則要求drawAll方法只能用于僅包含Shape對(duì)象的List,例如它不能用于List<Circle>。但不幸的是,由于所有的方法所做的只是從List中讀取Shape對(duì)象,所以它也需要能用于List<Circle>。我們所想要的就是這個(gè)方法能夠接受所有的 Shape類型。
public void drawAll(List<? extends Shape> shapes) {
...
}
這兒是一個(gè)很小但很重要的區(qū)別:我們已經(jīng)用List<? extends Shape>代替了List<Shape>?,F(xiàn)在,drawAll方法就可以接受Shape的任何子類對(duì)象的List了。
List<? extends Shape>就是有邊界的通配符的一個(gè)例子。問(wèn)號(hào)(?)代表未知類型,就如我們之前所看到的這個(gè)通配符一樣。然而,在這個(gè)例子中,我們這個(gè)未知類型實(shí)際上是Shape類的子類。(注:它可以是Shape類型本身;無(wú)需按字面上的意義一定說(shuō)是Shape子類)。
一般地,在使用通配符時(shí)要付出一些彈性方面的代價(jià)。這個(gè)代價(jià)就是,馬上向該方法體中寫入Shape類型的對(duì)象是非法的。例如,下面的代碼是不被允許的:
public void addRectangle(List<? extends Shape> shapes) {
shapes.add(0, new Rectangle()); // Compile-time error!
}
你應(yīng)該會(huì)指出為什么上面的代碼是不能被接受的。shaps.add的第二個(gè)參數(shù)是"? extends Shape"--一個(gè)未知的Shape子類,由于我們不知道它會(huì)是哪個(gè)Shape類型,不知道它的超類是否就是Rectangle;它可能是,也可能不是 Rectangle的超類,所以當(dāng)傳遞一個(gè)Rectangle對(duì)象,并不安全。
有邊界的通配符正是上一節(jié)中DMV向人口調(diào)查局提交數(shù)據(jù)的例子所需要的。我們的例子假設(shè)那些數(shù)據(jù)是由姓名(用字符串表示)到人(用Person或其子類,如 Driver,的引用類型表示)的映射表示。Map<K, V>是包含兩個(gè)類型參數(shù)的例子,這兩個(gè)類型參數(shù)分別表示映射中的鍵與值。
再次注意形式類型參數(shù)的命名規(guī)范--K代表鍵,V代表值。
public class Census {
public static void addRegistry(Map<String, ? extends Person> registry) {
}
...
Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);
5 泛型方法
考慮寫一個(gè)方法,它包含一個(gè)Object數(shù)據(jù)和一個(gè)集合對(duì)象,它的作用是將數(shù)組中的對(duì)象全部插入到集合對(duì)象中。下面是第一次嘗試:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // Compile time error
}
}
到現(xiàn)在為此,你要學(xué)會(huì)避免新手所犯的錯(cuò)誤--嘗試將Collection<Object>作為這個(gè)集合的類型參數(shù)。你可能認(rèn)識(shí)或沒(méi)認(rèn)識(shí)到使用 Collection<?>也不能完成工作。回憶一下,你不能將對(duì)象擠入一個(gè)未知類型的集合對(duì)象中。
處理這些問(wèn)題的方法是使用泛型方法。就像類型的聲明一樣,方法的聲明也可以泛型化--即,用一個(gè)或多個(gè)參數(shù)去參數(shù)化這個(gè)方法。
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}
我們能夠調(diào)用任意類型的集合對(duì)象中的方法,只要這個(gè)集合對(duì)象中的元素是數(shù)組類型中元素的超類型。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co); // T inferred to be 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
注意我們并不需要傳遞一個(gè)確切的類型給泛型方法。編譯器會(huì)根據(jù)準(zhǔn)確的參數(shù)的類型幫我們推斷出實(shí)際類型參數(shù)。編譯器通常會(huì)推斷出大部分的特定類型參數(shù),這就使得對(duì)方法的調(diào)用是類型正確的。
產(chǎn)生了一個(gè)問(wèn)題:什么時(shí)候我應(yīng)該使用泛型方法,什么時(shí)候我應(yīng)用使用通配符類型?為了理解答案,讓我們測(cè)試一些集合框架類庫(kù)中的方法:
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
我們可能使用下面的泛型方法替換上面的程序:
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
// Hey, type variables can have bounds too!
}
然而,在兩個(gè)containAll和addAll方法中,類型參數(shù)T只被使用了一次。返回類型既不依賴類型參數(shù),也不需要傳遞其它的參數(shù)給這個(gè)方法(在本例中,只不過(guò)是一個(gè)實(shí)參罷了)。這就告訴我們?cè)搶?shí)參將用于多態(tài);它的僅有的作用就是允許該方法的多種不同的實(shí)參能夠應(yīng)用于不同的調(diào)用點(diǎn)。
泛型方法允許類型參數(shù)用于描述一個(gè)或多個(gè)實(shí)參的類型對(duì)于該方法和/或它的返回值之間依賴關(guān)系。如果沒(méi)有這種依賴關(guān)系,那么就不應(yīng)該使用泛型方法。
一前一后的使用泛型方法和通配符是可能的,下面的方法Collections.copy()就表現(xiàn)了這一點(diǎn): class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
注意這兩個(gè)參數(shù)的類型之間的依賴關(guān)系。任何復(fù)制于源表scr的對(duì)象對(duì)于目標(biāo)表dest中元素的類型T都必須是可賦值的。所以src元素的類型肯定是T的任何子類型--我們不用關(guān)心這些。復(fù)制方法的簽名使用一個(gè)類型參數(shù)描述了這種依賴關(guān)系,但將通配符用于第二個(gè)參數(shù)中元素的類型。
我們也可以使用另一種方法來(lái)書(shū)寫這個(gè)方法的簽名,這種方法完全不需要使用通配符:
class Collections {
public static <T, S extends T>
void copy(List<T> dest, List<S> src) {
...
}
這很好,但當(dāng)?shù)谝粋€(gè)類型參數(shù)在類型dest和第二個(gè)類型的限度中都使用了時(shí),S它那本身只被使用了一次,就是在src的類型中--沒(méi)任何其它的東西再依賴于它了。這就是一個(gè)我們要以使用通配符替換S的一個(gè)信號(hào)。使用通配符比顯示的聲明類型變量更加清晰、更加精確,所以在任何可能的時(shí)候通配符是首選。
通配符也有它的優(yōu)點(diǎn),它可以被用于方法簽名的外面,以作為字段的類型,局部變量或數(shù)組。下面就是這樣的一個(gè)例子。
回到我們繪制形狀的那個(gè)例子,假設(shè)我們想維護(hù)一個(gè)繪制形狀請(qǐng)求的歷史記錄。我們可以將這個(gè)歷史記錄維護(hù)在類Shape內(nèi)部的一個(gè)靜態(tài)變量,讓drawAll方法將它自己獲得的實(shí)參(即要求繪制的形狀)加入歷史字段中。
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);
}
}
最后,仍然讓我們?cè)俅巫⒁忸愋妥兞康拿?guī)范。我們一般使用T表示類型,只要無(wú)需再區(qū)別任何其它的特定類型。這種情況經(jīng)常用于泛型方法中。如果有多個(gè)類型參數(shù),我可以使字母表中鄰近T的其它字母,例如S。如果在一個(gè)泛型類中有一個(gè)泛型方法,那么為了避免混淆,一個(gè)好的習(xí)慣是不要使泛型類和泛型方法有相同名字的類型參數(shù)。這也適用于嵌套泛型類。
6 與遺留代碼交互
到現(xiàn)在為止,我們的例子是假設(shè)處于一種理想的狀況,即每個(gè)人都在使用Java程序設(shè)計(jì)語(yǔ)言的支持泛型的最新版。
唉,但現(xiàn)實(shí)并非如此。數(shù)以百萬(wàn)行計(jì)的代碼是用Java語(yǔ)言的早期版本寫的,而且也不可能在一夜之間就將它們轉(zhuǎn)換到新版中。
稍后,在"使用泛型轉(zhuǎn)化遺留代碼"這一節(jié)中,我們將解決將你的舊代碼轉(zhuǎn)換到使用泛型這個(gè)問(wèn)題。在本節(jié),我們將關(guān)注一個(gè)簡(jiǎn)單的問(wèn)題:遺留代碼與泛型代碼之間如何交互?這個(gè)問(wèn)題含有兩個(gè)部分:在泛型代碼內(nèi)部使用遺留代碼;在遺留代碼內(nèi)部使用泛型代碼。
作為一個(gè)例子,假設(shè)你想使用包c(diǎn)om.Fooblibar.widgets。分支Fooblibar.com*商用在一個(gè)資產(chǎn)管理系統(tǒng)中,這個(gè)系統(tǒng)的精華如下所示:
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 parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
Collection getParts(); // Returns a collection of Parts
}
現(xiàn)在,你要添加一些新的代碼并使用上述API。比較好的是,要確保你一直能夠使用適當(dāng)?shù)膶?shí)參去調(diào)用addAssembly方法--即,你傳入的集合對(duì)象必須是裝有Part對(duì)象的集合對(duì)象。當(dāng)然,泛型最適合做這些了:
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方法時(shí),該方法希望第二個(gè)參數(shù)的類型是Collection。該參數(shù)的實(shí)際類型是Collection< Part>。這是正確的,但是什么呢?畢竟,大部分的Collection是不能包含Part對(duì)象的,因?yàn)橐话銇?lái)說(shuō),編譯器無(wú)法知道該 Collection所表示的是哪種對(duì)象的集合對(duì)象。
在合適的泛型代碼中,Collection將一直跟隨著一個(gè)類型參數(shù)。當(dāng)一個(gè)像Collection這樣的泛型類型在被使用時(shí)沒(méi)有提供類型參數(shù),就被稱之為原生類型(Raw Type)。
大多數(shù)人的每一直覺(jué)認(rèn)為Collection就是Collection<Object>。然而,按我們之前所說(shuō)的,在需要Collection<Object>的地方使用Collection<Part>并不是安全的。
但請(qǐng)等等,那也不對(duì)!想想對(duì)getParts對(duì)象的調(diào)用,它要返回一個(gè)Collection對(duì)象(實(shí)際上是一個(gè)引用變量)。然后這個(gè)對(duì)象被賦于變量k,k是 Collection<Part>類型。如果調(diào)用該方法而返回的結(jié)果是一個(gè)Collection<?>對(duì)象,該賦值操作也將產(chǎn)生錯(cuò)誤。
事實(shí)上,該賦值操作是合法的,它會(huì)生產(chǎn)一個(gè)未檢查的警告。這個(gè)警告是必要的,因?yàn)槭聦?shí)上編譯器并不能保證它的正確性。我們沒(méi)辦法檢查 getAssembly方法中的遺留代碼以保證返回的集合對(duì)象Part對(duì)象的集合。被用于該代碼的類型是Collection,能夠合法的向這種 Collection中插入任何類型的對(duì)象。
那么這還應(yīng)該是一個(gè)錯(cuò)誤嗎?就理論上而言,是的;但就實(shí)際上而言,如果泛型代碼是為了調(diào)用遺留代碼,那么就不得不允許了。對(duì)于你,一個(gè)程序員,會(huì)對(duì)這種情況感到滿意的,賦值是安全的,因?yàn)間etAssermbly方法的規(guī)則告訴我們它返回返回的是 Part對(duì)象的Collection,即使該方法的簽名并沒(méi)有表明這一點(diǎn)。
所以原生類型非常像通配符類型,但它們不會(huì)被做嚴(yán)格的類型檢查。這是經(jīng)過(guò)深思熟慮之后的結(jié)果,是為了允許泛型代碼能夠與之前已存在的代碼交互使用。
用泛型代碼調(diào)用遺留代碼是天生危險(xiǎn)的;一旦你在泛型代碼中混合了非泛型的遺留代碼,那么泛型類型系統(tǒng)通常都無(wú)法提供完全的保證。然而,這仍然比你不使用泛型要好些。至少你知道最終這些代碼是一致的。
碰到那兒已經(jīng)有了很多的非泛型代碼,然后又有了泛型代碼的時(shí)候,那么無(wú)法避免的情況就是不得不混合它們。
如果你發(fā)現(xiàn)你必須混合使用遺留代碼和泛型代碼,請(qǐng)密切注意未檢查的警告。要謹(jǐn)慎地思考你如何再才能證明那些被給出了危險(xiǎn)警告的代碼是安全的。
當(dāng)你繼續(xù)犯錯(cuò)誤,且代碼造成的警告確實(shí)不是類型安全的,什么事情將發(fā)生呢?讓我們看看這樣的一種情況。在這個(gè)處理過(guò)程中,我們將觀察編譯器所做的事情。
擦除和翻譯
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();
}
此處,我們已經(jīng)別名化了String的List和一個(gè)普通的老版的List。我們向這個(gè)List xs插入一個(gè)Integer對(duì)象,并試圖抽取一個(gè)String對(duì)象。這顯然是錯(cuò)的。如果我們忽略警告并嘗試執(zhí)行這段代碼,它將在我們?cè)噲D使用錯(cuò)誤類型的地方上失敗。
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)我們從這個(gè)List中抽取一個(gè)元素,并試圖將它當(dāng)作String對(duì)象而把它轉(zhuǎn)換成String時(shí),我們將得到一個(gè)ClassCastException的異常。完全相同的情況也發(fā)生在了loophole方法的泛型版中。
這種情況的原因就是泛型是由Java編譯器作為一種叫做"擦除(Erasure)"的最前到后的機(jī)制實(shí)現(xiàn)的。你(幾乎)可以把它想像為一種"源代碼對(duì)源代碼"(source-to-source)的翻譯,這就是為何loophole的泛型版被轉(zhuǎn)換成了非泛型版了。
結(jié)果,Java虛擬機(jī)的類型安全和完整性再也不處于危險(xiǎn)中了,甚至在遇到到未檢查的警告時(shí)也一樣。
基本地,Erasure去除(或者說(shuō)"擦除")了所有的泛型信息。所有的在角括號(hào)中的類型信息都被拋棄了,所以,如像List<String> 這樣的參數(shù)化類型被轉(zhuǎn)化成了List。所有保持對(duì)類型變量使用的地方都被類型變量的高層限度類型(一般就是Object)替換了。并且,無(wú)論何時(shí)產(chǎn)生的結(jié)果都不是類型正確的,一個(gè)向適當(dāng)?shù)念愋偷膹?qiáng)制類型轉(zhuǎn)換被插入了其中。
對(duì)Erasure的全部細(xì)節(jié)的描述超出了本教程的范疇,但我們給出的簡(jiǎn)單描述離真實(shí)情況并不太遠(yuǎn)。了解一些這方面的知識(shí)是有益的,特別是如果你想做一些更加老練的泛型應(yīng)用,如把已有的API轉(zhuǎn)換到使用泛型時(shí)(詳見(jiàn)"使用泛型轉(zhuǎn)化遺留代碼"),或者只是想理解為什么它們會(huì)是這種情況。
在遺留代碼中使用泛型代碼
現(xiàn)在讓我們思考一個(gè)顛倒的例子。想像Foolibar.com選擇泛型去轉(zhuǎn)化了它們的API,但他們的一些客戶端程序還沒(méi)有轉(zhuǎn)化。所以這些代碼看起來(lái)像:
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
}
客戶端程序看起來(lái)像:
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();
}
}
這些客戶端代碼是在泛型產(chǎn)生之前寫成的,但它使用了包c(diǎn)om.Fooblibar.widgets和集合框架類庫(kù),這兩者都在使用泛型??蛻舳酥袑?duì)泛型類型的使用使得它們成為了原生(Raw Type)類型。
代碼行1產(chǎn)生了一個(gè)未檢查的警告,因?yàn)橐粋€(gè)原生Collection被傳入了一個(gè)期望是Collection<Part>出現(xiàn)的地方,而且編譯器無(wú)法保證這個(gè)原生Collection真的就是Part對(duì)象的Collection。
作為一種可選的方法,你可以將這些代碼作為Java 1.4的源代碼進(jìn)行編譯,這就能保證不會(huì)出現(xiàn)警告。但這樣的話,你將不能使用到JDK 5.0中任何新的語(yǔ)言特性。
--------------------------------------------------------------------------
注意,"Fooblibar.com"是一個(gè)純屬虛構(gòu)的公司,目的僅僅只是為了本文中的例子。任何公司或機(jī)構(gòu)、任何健在或已故的個(gè)人與此有關(guān)的話,純屬巧合。
譯者:看來(lái)老外做事情十分謹(jǐn)慎,對(duì)于這種"小問(wèn)題"我們又怎么會(huì)如此鄭重其事的發(fā)表一個(gè)聲明呢。
7 良好的打印
一個(gè)泛型類被它的所有應(yīng)用共享
下面的代碼片斷是打印出什么呢?
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)橐粋€(gè)泛型類的所有實(shí)際擁有相同的運(yùn)行時(shí)類,而不管它們具體的類型參數(shù)。
確實(shí),對(duì)一個(gè)類的泛型所做的事實(shí)就是這個(gè)泛型類對(duì)它所有可能的類型參數(shù)都有相同的行為;相同的這個(gè)類可以被視為它有很多不同的類型。
同樣的結(jié)果,泛型類中的靜態(tài)變量和方法也被該類的所有實(shí)例共享。這就是為什么在一個(gè)靜態(tài)方法或初始化器中、在一個(gè)靜態(tài)變量的聲明或初始化器中引用類型變量是非法的。
Cast和Instanceof
一個(gè)泛型類被它的所有實(shí)例共享的另一個(gè)隱含意義就是,如果某個(gè)實(shí)例是這個(gè)泛型類的一種特定類型的實(shí)例,那么通常情況下請(qǐng)求這個(gè)類的實(shí)例是無(wú)意義的:
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // Illegal.
類似地,如下面這個(gè)強(qiáng)制類型轉(zhuǎn)換
Collection<String> cstr = (Collection<String>) cs; // Unchecked warning,
會(huì)報(bào)一個(gè)未檢查的警告,因?yàn)檫@不應(yīng)該是運(yùn)行時(shí)系統(tǒng)將要為你檢查的事情。
對(duì)類型變量也是如此
<T> T badCast(T t, Object o) {return (T) o; // Unchecked warning.
}
類型變量在運(yùn)行時(shí)并不存在。這就意味著在時(shí)間和空間上,它們都不可能避免地?zé)o法產(chǎn)生作用。不幸的是,這也意味著你不能可靠地在強(qiáng)制類型轉(zhuǎn)換中使用它們。
數(shù)組
一個(gè)數(shù)組對(duì)象中元素的類型不會(huì)是一個(gè)類型變量或參數(shù)化的類型,除非它是一個(gè)(非受限的)通配符類型。你可以聲明數(shù)組類型的元素類型是一個(gè)類型變量或參數(shù)化的類型,但數(shù)組對(duì)象本身不行。
這很煩人,但卻是真的。該約束對(duì)避免如下例子中的情況是有必要的:
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ù)組,上面的例子將會(huì)通過(guò)編譯且不報(bào)任何未檢查的警告,然而會(huì)在運(yùn)行時(shí)失敗。我們已經(jīng)知道設(shè)計(jì)泛型的主要目的就是為了類型安全。特別地說(shuō),Java語(yǔ)言被設(shè)計(jì)為,如果你的整個(gè)程序使用javac -source 1.5進(jìn)行編譯時(shí)沒(méi)有報(bào)任何未檢查的警告,那么這個(gè)程序就是類型安全的。
然而,你仍然可以使用通配符數(shù)組。這兒有上面代碼的兩個(gè)變種。第一個(gè)變種放棄使用參數(shù)化類型的數(shù)組對(duì)象和參數(shù)化類型元素。這樣我們?yōu)榱嗽跀?shù)組外得到String對(duì)象不得不在顯示地使用強(qiáng)制類型轉(zhuǎn)換。
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.
在第二個(gè)變種中,我們限制了數(shù)組對(duì)象的創(chuàng)建,這個(gè)數(shù)組的元素的類型被參數(shù)化了,但仍然要將一個(gè)參數(shù)化的元素類型用于這個(gè)數(shù)組。這是合法的,但產(chǎn)生一個(gè)未檢查的警告。確實(shí),這段代碼是不安全的,甚至?xí)?dǎo)致一個(gè)錯(cuò)誤。
List<String>[] lsa = new List<?>[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.
譯者:根據(jù)我的測(cè)試(JDK 1.5.0_11),"List<String>[] lsa = new List<?>[10]"這一句無(wú)法通過(guò)編譯,理由也很直觀"類型不匹配,不能將List<?>[]轉(zhuǎn)化為L(zhǎng)ist< String>[]"。
類似地,試圖創(chuàng)建一個(gè)元素類型是類型變量的數(shù)組對(duì)象會(huì)導(dǎo)致一個(gè)運(yùn)行時(shí)錯(cuò)誤:
<T> T[] makeArray(T t) {
return new T[100]; // Error.
}
因?yàn)轭愋妥兞吭谶\(yùn)行時(shí)并不存在,這就沒(méi)有辦法確定數(shù)組的實(shí)際類型。
圍繞著這些限制的工作方法是使用了將類字面量當(dāng)作運(yùn)行時(shí)類型標(biāo)記的機(jī)制,該機(jī)制將在下一節(jié)"類字面量作為運(yùn)行時(shí)標(biāo)記"中進(jìn)行敘述。
8 類字面量作為運(yùn)行時(shí)標(biāo)記
JDK 5.0的變量之一就是java.lang.Class也被泛型化了。這是一個(gè)不容器類而在其它地方使用泛型機(jī)制的有趣例子。
既然Class類有一個(gè)類型參數(shù)T,你可能會(huì)問(wèn),這個(gè)T代表什么?它代表這個(gè)Class對(duì)象表示的類型。
例如,String.class的類型是Class<String>,而Serializable.class的類型就是Class<Serializable>。這種機(jī)制用于提高在你的反射程序中的類型安全性。
特別地,由于Class類中的方法netInstance現(xiàn)在是返回一個(gè)T,這樣當(dāng)你在使用反射機(jī)制創(chuàng)建對(duì)象時(shí)能夠得到更加精確的類型。
例如,假設(shè)你需要一個(gè)執(zhí)行數(shù)據(jù)庫(kù)查詢的工具方法,給入的是SQL字符串,返回的是數(shù)據(jù)庫(kù)中匹配該查詢語(yǔ)言的對(duì)象的集合。
一種方法就是顯示地傳入一個(gè)工廠對(duì)象中,所寫的代碼就像:
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc */
for (/* 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類去支持Factory接口
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() { return new EmpInfo();}
}
然后像下面那樣去調(diào)用它
select(getMyEmpInfoFactory(), "selection string");
這個(gè)解決方案最終還需要:
* 在調(diào)用點(diǎn)使用冗長(zhǎng)的匿名工廠類,
* 或者,為每個(gè)被使用的類型聲明一個(gè)工廠類,并將這個(gè)工廠類的實(shí)例傳遞到調(diào)用點(diǎn),但這種方法有點(diǎn)不自然。
可以很自然地將類字面量用作工廠對(duì)象,這個(gè)工廠稍后可被反射機(jī)制使用?,F(xiàn)在這個(gè)程序(不用泛型)可以寫為:
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è)所期望的精確類型的集合。既然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>();
/* 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;
}
上面的程序以一種類型安全的方法給了我們精確類型的集合。
將類字面量作為運(yùn)行時(shí)標(biāo)記的技術(shù)被認(rèn)為十分狡猾。例如,為了操作Annotation,這種技術(shù)在新API中被擴(kuò)展使用了。
9 通配符的更多趣味
在本節(jié),我們將考慮一些更高級(jí)的通配符用法。我們已經(jīng)看了幾個(gè)受限的通配符用于讀取數(shù)據(jù)結(jié)構(gòu)時(shí)例子?,F(xiàn)在反過(guò)來(lái)想想一個(gè)只可寫的數(shù)據(jù)結(jié)構(gòu)。接口Sink是這種類型的一個(gè)簡(jiǎn)單的例子:
interface Sink<T> {
flush(T t);
}
我們可以想像將它作為一個(gè)范例用于下面的代碼。方法writeAll被設(shè)計(jì)為刷新集合coll中的所有元素到Sink的實(shí)例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); // Illegal call.
就已經(jīng)寫出來(lái)的,對(duì)writeAll方法的調(diào)用是非法的,由于無(wú)法推斷出有效的類型實(shí)參;String或Object都不是T的合適類型,因?yàn)镃ollection的元素和Sink必須是相同的類型。
我們可以通過(guò)修改writeAll的方法簽名來(lái)修正這個(gè)錯(cuò)誤,如下所示,使用了通配符:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
String str = writeAll(cs, s); // Call is OK, but wrong return type.
該調(diào)用是合法的,但賦值是錯(cuò)的,是由于返回類型被推斷成了Object,因?yàn)門匹配s的類型,但s的類型是Object。
該解決方案使用了一種我們尚未見(jiàn)過(guò)的受限通配符形式:有一個(gè)較低限度的通配符。語(yǔ)法"? super T"表示未知類型是T的超類型(或者是T本身;記住,超類型關(guān)系是彈性的)。
public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
...
}
String str = writeAll(cs, s); // Yes!
使用了這種語(yǔ)法,方法的調(diào)用就是合法的,并且被推斷的類型正如所愿是String。
現(xiàn)在讓我們轉(zhuǎn)向更為實(shí)際的例子。java.util.TreeSet<E>表示了一個(gè)排序了的以類型為E的對(duì)象作為元素的樹(shù)。構(gòu)造一個(gè) TreeSet對(duì)象的方法之一是傳遞一個(gè)Comparator對(duì)象給這個(gè)構(gòu)造器。該Comparator對(duì)象將被用于根據(jù)期望的規(guī)則對(duì)TreeSet中的元素進(jìn)行排序。
TreeSet(Comparator<E> c)
Comparator接口是必須的:
interface Comparator<T> {
int compare(T fst, T snd);
}
假設(shè)我們想創(chuàng)建一個(gè)TreeSet<String>對(duì)象,并傳入一個(gè)合適的比較器對(duì)象。我們就需要一個(gè)能比較String的 Comparator對(duì)象,一個(gè)Comparator<String>就可以做到,但一個(gè)Comparator<Object> 對(duì)象也能做到。然而,我們不能調(diào)用上面Comparator<Object>所提供的構(gòu)造器。
TreeSet(Comparator<? super E> c)
上述代碼允許適用的比較器被使用。
作為最后一個(gè)低位受限通配符的例子,讓我們看看Collections.max方法,該方法返回一個(gè)集合中的極大元素。為了讓max文件能夠工作,集合中所有的傳入該集合的元素都必須實(shí)現(xiàn)了Comparable接口。此外,它們相互之間必須是可被比較的。
在第一次嘗試創(chuàng)建這個(gè)方法后有如下結(jié)果:
public static <T extends Comparable<T>>
T max(Collection<T> coll)
即,這個(gè)方法有一個(gè)某類型T的集合對(duì)象,T的實(shí)例之間可以進(jìn)行比較,該方法并返回一個(gè)該類型的元素。然而,這個(gè)程序?qū)崿F(xiàn)起來(lái)太受限制了。看看是為什么,考慮一個(gè)對(duì)象,它能與任意對(duì)象進(jìn)行比較:
class Foo implements Comparable<Object> {
...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // Should work.
Collection cf中的每個(gè)元素都能與該集合中的其它元素進(jìn)行比較,因?yàn)槊總€(gè)這樣的元素都是一個(gè)Foo的實(shí)例,而Foo的實(shí)例能夠與任意對(duì)象進(jìn)行比較,則與另一個(gè)Foo 對(duì)象比較那就更沒(méi)問(wèn)題了。然而,使用前面的方法簽名,我們可以發(fā)現(xiàn)上面對(duì)方法max的調(diào)用會(huì)被拒絕。被推斷出的類型必須是Foo,但Foo并沒(méi)有實(shí)現(xiàn) Comparable<Foo>。
沒(méi)有必要精確地要求T與它自己的實(shí)例進(jìn)行比較。所有被要求的是T的實(shí)例能夠與它的某個(gè)超類型的實(shí)例進(jìn)行比較。這就讓我們有了如下代碼:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
注意到Collections.max真實(shí)的方法簽名更難以理解。我們將在下一節(jié)"將遺留代碼轉(zhuǎn)化到使用泛型"中再講述它。這個(gè)適用于幾乎任何一個(gè) Comprarable應(yīng)用的理論是打算能用于任意的類型:你總是想使用Comprarable<? super T>。
一般地,如果你的API只是將類型參數(shù)T作為類型變量使用,那就應(yīng)該利于低位受限通配符(? super T)。相反地,如果這個(gè)API只需返回T,你就要使用高位受限通配符(? extends T)以給這個(gè)API的客戶端程序更大的靈活性。
通配符捕獲
到目前為此,下面的程序應(yīng)該更清晰些:
Set<?> unknownSet = new HashSet<String>();
...
/** Add an element t to a Set s. */
public static <T> void addToSet(Set<T> s, T t) {
...
}
但下面的調(diào)用是非法的。
addToSet(unknownSet, "abc"); // Illegal.
傳入該方法的一個(gè)精確的Set是一個(gè)String的Set這沒(méi)有影響;問(wèn)題在于作為實(shí)參傳入表達(dá)式的是一個(gè)未知類型的Set,這并不能保證它一定就是String或其它任何特定類型的Set。
現(xiàn)在考慮下面的代碼:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?
看起來(lái)它應(yīng)該不被允許;然而,看看這個(gè)特殊的調(diào)用,它確實(shí)是安全的而可以允許這么做。畢竟,unmodifiableSet方法可用于任何類型的Set,而不管這個(gè)Set中的元素的類型。
因?yàn)檫@種情況發(fā)生地相對(duì)比較頻繁,所以有一個(gè)特殊的規(guī)則允許這些在一個(gè)非常特殊的環(huán)境中的代碼是合法的,在這個(gè)環(huán)境中這些代碼被證明是安全的。這個(gè)名為"通配符捕獲"的規(guī)則允許編譯器將通配符的未知類型作為類型實(shí)參推斷到泛型方法中。
10 將遺留代碼轉(zhuǎn)化為使用泛型
早先,我們展示了新、老代碼之間如何交互。現(xiàn)在是時(shí)候看看"泛型化"老代碼這個(gè)困難的問(wèn)題了。
如果你決定將老代碼轉(zhuǎn)換成使用泛型,你需要仔細(xì)考慮如何去修改你的API。
你需要確定泛型化的API不會(huì)造成過(guò)度的限制;它必須能繼續(xù)地支持API原先的功能。再次考慮一些來(lái)自于java.util.Collection中的例子。沒(méi)有使用泛型的API看起來(lái)像:
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);
}
肯定是類型安全的了,但它并沒(méi)有實(shí)現(xiàn)該API之前的功能。containsAll方法用于任何引入的集合對(duì)象,如果引入的集合真地僅包含E的實(shí)例時(shí),該方法才會(huì)成功。但是:
* 引入集合的靜態(tài)類型可能有所不同,或許是因?yàn)檎{(diào)用者不知道傳入的集合對(duì)象的準(zhǔn)確類型,或者可能是因?yàn)樗且粋€(gè)Collection<S>,而S是E的子類型。
* 能夠合法地使用一個(gè)不同的類型的集合調(diào)用containsAll方法則最為理想了。這種方法應(yīng)該能工作,并將返回false。
在這個(gè)例子中的addAll方法,我們應(yīng)該能夠加入由任何由E的子類型的實(shí)例組成的集合對(duì)象。我們?cè)?泛型方法"這一節(jié)中已經(jīng)看過(guò)了如何正確地處理此類情況。
你也需要保證修改后的API要保持與老的客戶端程序的二進(jìn)制兼容性。這就暗示著"擦除"后的API必須與以前的非泛型化API相同。在大部分例子中,這自然會(huì)引用爭(zhēng)吵,但也有一些精妙的例子。我們將測(cè)試我們已經(jīng)遇到過(guò)的最精妙例子中的一個(gè),即Collections.max()方法。根據(jù)我們?cè)?通配符的更多樂(lè)趣"一節(jié)所看到的,一個(gè)模糊的max方法簽名是:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
除了擦除后的簽名之外,這些都很好:
public static Comparable max(Collection coll)
這與max之前的方法簽名不同:
public static Object max(Collection coll)
當(dāng)然可以這樣指定max方法的簽名,但這沒(méi)有什么用。所有老的調(diào)用Collections.max方法的二進(jìn)制class文件都依賴于返回類型為Object的方法的簽名。
通過(guò)顯示地在限度中為形式類型參數(shù)T指定一個(gè)超類,我們能夠強(qiáng)制這個(gè)擦除產(chǎn)生不同的結(jié)果。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
這是一個(gè)單個(gè)類型參數(shù)有多個(gè)限度的例子,使用語(yǔ)法"T1 & T2 ... & Tn"。有多個(gè)限度的類型變量是被認(rèn)為是限度中所有類型的一個(gè)子類型。當(dāng)使用多限度時(shí),限度中第一個(gè)被提及的類型將作為該類型變量被擦除后的類型。
最后,我們應(yīng)該回想到max方法只需從輸入的Collection中進(jìn)行讀取操作,所以這適合于T的任何子類型的集合。
這就把我們帶入到JDK中該方法的真實(shí)簽名中:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在實(shí)踐中產(chǎn)生如此晦澀的應(yīng)用是十分罕見(jiàn)的,但是當(dāng)轉(zhuǎn)換現(xiàn)有API時(shí),專家型的類庫(kù)設(shè)計(jì)者們應(yīng)該要準(zhǔn)備著去進(jìn)行非常細(xì)致地地思考。
另一個(gè)問(wèn)題需要密切關(guān)注的就是"協(xié)變返回",即在一個(gè)子類型中精煉了返回類型。你不需要在老的API中使用這個(gè)特性。為了找到原因,讓我們看一個(gè)例子。
假設(shè)你原先的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)在假設(shè)有一個(gè)像下面那樣寫的你代碼的第三方客戶端程序:
public class Baz extends Bar {
public Foo create() {
...
} // Actually creates a Baz.
}
Java 虛擬機(jī)不直接支持有著不同返回類型的方法的覆蓋,該特性由編譯器支持。因此,除非Baz類被重新編譯,否則它不能正常地覆蓋Bar的create方法。另外,Baz將不得不被修改,因?yàn)檫@些代碼將如前面所寫的那樣被拒絕--Baz中的create方法返回類型并不是Bar中create方法返回類型的子類型。
譯者:根據(jù)我的測(cè)試(JDK 1.5.0_11),Baz類中的create方法無(wú)法通過(guò)編譯,理由就是Baz.create方法與Bar.create方法的返回不兼容,返回類型須是Bar,而不是Foo。
致謝
Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ahe和Philip Wadler為該教程提供了材料。
感謝David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, Orjan Petersson, Scott Seligman, Yoshiki Shibata和Kresten Krab Thorup為該教程的早期版本所提出的富有價(jià)值的反饋。向我忘記列出來(lái)的每個(gè)人道歉。
posted on 2007-06-20 11:11 John Jiang 閱讀(4339) 評(píng)論(17) 編輯 收藏 所屬分類: JavaSE 、Generics 、翻譯 、JavaTutorials