此文為doyouj來(lái)自http://bbs.itebook.net/
感謝doyouj

[翻譯]Java泛型編程指南

此系列文章譯自SUN的泛型編程指南, 看不懂譯文的請(qǐng)看原文
http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf

一、緒言
JDK1.5對(duì)JAVA語(yǔ)言進(jìn)行了做了幾個(gè)擴(kuò)展,其中一個(gè)就是泛型。
本指南旨在介紹泛型。如果你熟悉其它語(yǔ)言的構(gòu)造類似的東西,特別是C++的模
板(template),你會(huì)很快發(fā)現(xiàn)它們之間的相同點(diǎn)及重要的不同點(diǎn);如果你在其他
地方?jīng)]看到過(guò)類似的東西,那反而更好,那樣你就可以開始全新的學(xué)習(xí),用不著去忘
掉那些(對(duì)JAVA泛型)容易產(chǎn)生誤解的東西。
泛型允許你對(duì)類型進(jìn)行抽象。最常見(jiàn)的例子是容器類型,比如那些在Collection
層次下的類型。
下面是那類例子的典型用法:

List myIntList = new LinkedList();//1
myIntList.add(new Integer(0));//2
Integer x = (Integer) myIntList.iterator().next();//3

第3行里的強(qiáng)制類型轉(zhuǎn)換有點(diǎn)煩人,程序通常都知道一個(gè)特定的鏈表(list)里
存放的是何種類型的數(shù)據(jù),但卻一定要進(jìn)行類型轉(zhuǎn)換。編譯器只能保證迭代器返回的
是一個(gè)對(duì)象,要保證對(duì)Integer類型變量的賦值是類型安全的話,必須進(jìn)行類型轉(zhuǎn)換。
類型轉(zhuǎn)換不但會(huì)引起程序的混亂,還可能會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤,因?yàn)槌绦騿T可能會(huì)
犯錯(cuò)誤。
如果程序員可以如實(shí)地表達(dá)他們的意圖,即標(biāo)記一個(gè)只能包含特定數(shù)據(jù)類型的鏈
表,那會(huì)怎么樣呢?這就是泛型背后的核心思想。下面是前面代碼的泛型寫法:
List<Integer> myIntList = new LinkedList<Integer>();//1'
myIntList.add(new Integer(0));//2'
Integer x = myIntList.iterator().next();//3'

請(qǐng)注意變量myIntList的類型聲明,它指明了這不僅僅是一個(gè)任意的List,還
是一個(gè)Integer類型的List,寫作List<Integer>。我們說(shuō)List是一個(gè)接受類型(在
這個(gè)例子是Integer)參數(shù)的泛華的接口,在創(chuàng)建鏈表對(duì)象的時(shí)候,我們也指定了一個(gè)
類型參數(shù)。
另外要注意的是在第3'行的類型轉(zhuǎn)換已經(jīng)不見(jiàn)了。
現(xiàn)在你可能會(huì)想,我們所做的全部都是為了把混亂消除。我們沒(méi)有在第3行把類
型轉(zhuǎn)換為Integer,而是在第1'行加了Integer類型參數(shù);非也非也,這里面差別很
大,編譯器現(xiàn)在能夠在編譯期間檢測(cè)程序的類型正確性。當(dāng)我們把myIntList聲明為
類型List<Integer>的后,就意味著變量myIntList在何時(shí)何地的使用都是正確的,
編譯器保證了這一點(diǎn)。相反,類型轉(zhuǎn)換只是告訴我們程序員認(rèn)為它在程序的某個(gè)地方
是正確的。
實(shí)際的結(jié)果是,程序(特別是大型的程序)的可讀性和健壯性得到了提高。
-----------------------------------------------------------------------------------------------------
二、定義簡(jiǎn)單的泛型

下面是java.util包里的List和Iterator接口定義的一個(gè)小小的引用:

public interface List<E>{
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E>{
E next();
boolean hasNext();
}

除了尖括號(hào)里的東西,這里所有的都應(yīng)該很熟悉了。那是List和Iterator接口
的規(guī)范類型參數(shù)的聲明。
類型參數(shù)可以用在任何的泛型聲明中,就像使用普通的類型一樣(雖然有一些很
重要的限制;看第7部分)。
在緒言中,我們看到了List泛型聲明的調(diào)用,比如List<Integer>。在調(diào)用里面
(通常稱為參數(shù)化類型),所有出現(xiàn)規(guī)范類型參數(shù)(這里是E)的全部都用實(shí)際的類型
參數(shù)(這里是Integer)所代替。
你可以想象成List<Integer>代表所有E都用Integer代替了的List:

public interface IntegerList{
void add(Integer x)
Iterator<Integer> iterator();
}

這種想法是有所幫助的,但也會(huì)造成誤解。
它是有所幫助的,是因?yàn)閰?shù)化類型List<integer>有看起來(lái)像這種擴(kuò)展的方法。
它會(huì)造成誤解,是因?yàn)榉盒偷穆暶鲗?shí)際上不會(huì)像那樣去擴(kuò)展;在源代碼中、二進(jìn)制
文件中、硬盤和內(nèi)在里,都沒(méi)有代碼的多個(gè)拷貝。如果你是一個(gè)C++程序員,你會(huì)明白
這跟C++的模板(template)很不同。
泛型聲明是一次編譯,永遠(yuǎn)使用,它會(huì)變成一個(gè)單獨(dú)的class文件,就像一個(gè)普通
的類或接口聲明。
類型參數(shù)跟用在方法或構(gòu)造函數(shù)里的普通的參數(shù)類似,就像一個(gè)方法具有描述它運(yùn)
算用到的值的類型的規(guī)范值參一樣,泛化聲明具有規(guī)范類型參數(shù)。當(dāng)一個(gè)方法被調(diào)用的
時(shí)候,實(shí)際的參數(shù)將會(huì)被規(guī)范參數(shù)所代替而對(duì)方法求值。當(dāng)一個(gè)泛化聲明被調(diào)用的時(shí)候,
實(shí)際類型參數(shù)將會(huì)代替規(guī)范類型參數(shù)。
命名慣例要注意的一個(gè)地方。我們建議你用一些簡(jiǎn)煉(如果可以的話只用一個(gè)字
符)但卻映眼的名字作為規(guī)范類型參數(shù)名。在那些名字中最后避免小寫字母,這樣可
以很容易把規(guī)范類型參數(shù)和普通的類或接口區(qū)分開來(lái)。就像前面的例子一樣,很多容
器類型使用E。我們將會(huì)在后面的例子里看到其他的慣例。
----------------------------------------------------------------------------------------------------------------
三、泛型和子類化

http://xoj.blogone.net

我們來(lái)測(cè)試一下對(duì)泛型的理解,下面的代碼是否正確呢?

List<String> ls = new ArrayList<String>();//1
List<Object> lo = ls;//2

第1行肯定是正確的,問(wèn)題的難點(diǎn)在于第2行;這樣就歸結(jié)為這個(gè)問(wèn)題:一個(gè)字符
串(String)鏈表(List)是不是一個(gè)對(duì)象鏈表?大部分人的直覺(jué)是:“肯定了!”
那好,看一下下面這兩行:

lo.add(new Object());//3
String s = ls.get(0);//4:企圖把一個(gè)對(duì)象賦值給字符串!

在這里我們把ls和lo搞混淆了。我們通過(guò)別名lo來(lái)訪問(wèn)字符串鏈表ls,插入不
確定對(duì)象;結(jié)果就是ls不再存儲(chǔ)字符串,當(dāng)我們嘗試從里面取出數(shù)據(jù)的時(shí)候就會(huì)出錯(cuò)。
Java編譯器當(dāng)然不允許這樣的事情發(fā)生了,所以第2行肯定會(huì)編譯出錯(cuò)。
一般來(lái)說(shuō),如果Foo是Bar的子類型(子類或子接口),而G又是某個(gè)泛型聲明的
話,G<Foo>并不是G<Bar>的子類型。這可能是學(xué)習(xí)泛型的時(shí)候最難的地方,因?yàn)樗?br />與我們的深層直覺(jué)相違背。
直覺(jué)出錯(cuò)的問(wèn)題在于它把集合里的東西假想為不會(huì)改變的,我們的本能把這些東
西看作是不變的。
舉個(gè)例子,假設(shè)汽車公司為人口調(diào)查局提供一份駕駛員的列表,這看上去挺合理。
假設(shè)Driver是Person的一個(gè)子類,則我們認(rèn)為L(zhǎng)ist<Driver>是一個(gè)List<Person>。
而實(shí)際上提交的是一份駕駛員登記表的一個(gè)副本。否則的話,人口調(diào)查局將可以駕駛員
的人加入到那份列表中去,汽車公司的紀(jì)錄受到破壞。
為了解決這類問(wèn)題,我們需要考慮一些更靈活的泛型,到現(xiàn)在為止碰到的規(guī)則太
受約束了。
---------------------------------------------------------------------------------------------------
四、通配符

http://xoj.blogone.net

考慮一下寫一個(gè)程序來(lái)打印一個(gè)集合對(duì)象(collection)里的所有元素。
在舊版的語(yǔ)言里面,你可以會(huì)像下面那樣寫:

void printCollection(Collection c){
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++){
System.out.println(i.next());
}
}

下面嘗試著用泛型(和新的for循環(huán)語(yǔ)法)來(lái)寫:

void printCollection(Collection<Object> c){
for (Object e : c) {
System.out.println(e);
}
}

這樣的問(wèn)題是新版本的代碼還沒(méi)舊版本的代碼好用。就像我們剛示范的一樣,
Collection<Object>并不是所有類型的集合的父類型,所以它只能接受Collection<Object>
對(duì)象,而舊版的代碼卻可以把任何類型的集合對(duì)象作為參數(shù)來(lái)調(diào)用。
那么,什么才是所有集合類型的父類型呢?這個(gè)東西寫作Collection<?>(讀
作“未知集合”),就是元素類型可以為任何類型的集合。這就是它為什么被稱為“通
配符類型”的原因。我們可以這樣寫:

void printCollection(Collection<?> c){
for (Object e : c) {
System.out.println(e);
}
}

現(xiàn)在,我們就可以以任何類型的集合對(duì)象作為參數(shù)來(lái)調(diào)用了。注意,在printCollection()
方法里面,我們?nèi)匀豢梢詮腸對(duì)象中讀取元素并賦予Object類型;因?yàn)闊o(wú)論集合里
實(shí)際包含了什么類型,它肯定是對(duì)象,所以是類型安全的。但對(duì)它插入任意的對(duì)象
的話則是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object());//編譯錯(cuò)誤

由于我們并不知道c的元素類型是什么,因此我們不能對(duì)其插入對(duì)象。add()方法
接受類型E,即集合的元素類型的參數(shù)。當(dāng)實(shí)際的類型參數(shù)是?的時(shí)候,就代表是某未
知類型。任何傳遞給add方法的參數(shù),其類型必須是該未知類型的子類型。因?yàn)槲覀儾?br />不知道那是什么類型,所以我們傳遞不了任何參數(shù)。唯一的例外就是null,因?yàn)樗侨?br />何(對(duì)象)類型的成員。
另外,假設(shè)有一個(gè)List<?>,我們可以調(diào)用get()方法并使用其返回結(jié)果。結(jié)果
類型是一個(gè)未知類型,但我們都知道它是一個(gè)對(duì)象。因此把get()方法的返回結(jié)果賦
值給對(duì)象類型,或者把它作為一個(gè)對(duì)象參數(shù)傳遞都是類型安全的。
----------------------------------------------------------------------------------------------------
四、1-有界通配符

http://xoj.blogone.net

考慮一個(gè)簡(jiǎn)單的畫圖程序,它可以畫長(zhǎng)方形和圓等形狀。為了表示這些形狀,
你可能會(huì)定義這樣的一個(gè)類層次結(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) { ... }
}

這些類可以在canvas上描畫:

public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}

任何的描畫通常都包括有幾種形狀,假設(shè)它們用一個(gè)鏈表來(lái)表示,那么如果在
Canvas里面有一個(gè)方法來(lái)畫出所有的形狀的話,那將會(huì)很方便:

public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}

但是現(xiàn)在,類型的規(guī)則說(shuō)drawAll()方法只能對(duì)確切的Shape類型鏈表調(diào)用,
比如,它不能對(duì)List<Circle>類型調(diào)用該方法。那真是不幸,因?yàn)檫@個(gè)方法所要
做的就是從鏈表中讀取形狀對(duì)象,從而對(duì)List<Circle>類型對(duì)象進(jìn)行調(diào)用。我們
真正所想的是要讓這個(gè)方法能夠接受一個(gè)任何形狀的類型鏈表:

public void drawAll(List<? extends Shape> shapes) { ... }

這里有一個(gè)很小但很重要的不同點(diǎn):我們把類型List<Shape>替換為L(zhǎng)ist<? extends Shape>。
現(xiàn)在drawAll()方法可以接受任何Shape子類的鏈表,我們就可以如愿的對(duì)List<Circle>
調(diào)用進(jìn)行啦。
List<? extends Shape>是一個(gè)有界通配符的例子。? 表示一個(gè)未知類型,
就像我們之前所看到的通配符一樣。但是,我們知道在這個(gè)例子里面這個(gè)未知類型
實(shí)際是Shape的子類型(注:它可以是Shape本身,或者是它的子類,無(wú)須在字面上
表明它是繼承Shape類的)。我們說(shuō)Shape是通配符的“上界”。
如往常一樣,使用通配符帶來(lái)的靈活性得要付出一定的代價(jià);代碼就是現(xiàn)在在
方法里面不能對(duì)Shape對(duì)象插入元素。例如,下面的寫法是不允許的:

public void addRectangle(List<? extends Shape> shapes) {
shapes.add(0, new Rectangle()); //編譯錯(cuò)誤
}

你應(yīng)該可以指出為什么上面的代碼是不允許的。shapes.add()方法的第二個(gè)
參數(shù)的類型是 ? 繼承Shape,也就是一個(gè)未知的Shape的子類型。既然我們不知道
類型是什么,那么我們就不知道它是否是Rectangle的父類型了;它可能是也可能
不是一個(gè)父類型,因此在那里傳遞一個(gè)Rectangle的對(duì)象是不安全的。
有界通配符正是需要用來(lái)處理汽車公司給人口調(diào)查局提交數(shù)據(jù)的例子方法。在
我們的例子里面,我們假設(shè)數(shù)據(jù)表示為姓名(用字符串表示)對(duì)人(表示為引用類
型,比如Person或它的子類型Driver等)的映射。Map<K, V>是有兩個(gè)類型參數(shù)的
一個(gè)泛型的例子,表示鍵值映射。
請(qǐng)?jiān)僖淮巫⒁庖?guī)范類型參數(shù)的命名慣例:K表示鍵,V表示值。

public class Census {
public static void
addRegistry(Map<String, ? extends Person> registry){ ... }
}
...
Map<String, Driver> allDrivers = ...;
Census.addRegistry(allDrivers);
-------------------------------------------------------------------------------------------
五、泛型方法

http://xoj.blogone.net

考慮寫這樣一個(gè)方法,它接收一個(gè)數(shù)組和一個(gè)集合(collection)作為參數(shù),
并把數(shù)組里的所有對(duì)象放到集合里面。
先試試這樣:

static void fromArrayToCollection(Object[] a, Collection<?> c){
for (Object o : a){
c.add(o);//編譯錯(cuò)誤
}
}

到現(xiàn)在,你應(yīng)該學(xué)會(huì)了避免把Collection<Object>作為集合參數(shù)的類型這種初學(xué)
者的錯(cuò)誤;你可能或可能沒(méi)看出使用Collection<?>也是不行的,回想一下,你是不能
把對(duì)象硬塞進(jìn)一個(gè)未知類型的集合里面的。
解決這類問(wèn)題的方法是使用泛型方法。就像類型聲明一樣,方法也可以聲明為泛型
的,就是說(shuō),用一個(gè)或多個(gè)類型參數(shù)作為參數(shù)。

static <T> void fromArrayToCollection(T[]a, Collection<T> c){
for (T o : a){
c.add(o);//正確
}
}

對(duì)于集合元素的類型是數(shù)組類型的父類型,我們就可以調(diào)用這個(gè)方法。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T是對(duì)象類型
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
fromArrayToCollection(sa, cs);// T是字符串類型(String)
fromArrayToCollection(sa, co);// T對(duì)象類型
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是Number類型
fromArrayToCollection(fa, cn);// T是Number類型
fromArrayToCollection(na, cn);// T是Number類型
fromArrayToCollection(na, co);// T是Number類型
fromArrayToCollection(na, cs);// 編譯錯(cuò)誤

請(qǐng)注意,我們并沒(méi)有把實(shí)際的類型實(shí)參傳遞給泛型方法,因?yàn)榫幾g器會(huì)根據(jù)
實(shí)參的類型為我們推斷出類型實(shí)參。一般地,編譯器推斷得到可以正確調(diào)用的最
接近的(the most specific)實(shí)參類型。
現(xiàn)在有一個(gè)問(wèn)題:我應(yīng)該什么時(shí)候使用泛型方法,什么時(shí)候使用通配符類型
呢?為了明白這個(gè)問(wèn)題的答案,我們來(lái)看看Collection庫(kù)里的幾個(gè)方法:

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 <? extends E>boolean addAll(Collection<T> c);
//哈哈,類型變量也可以有界!
}

但是,類型參數(shù)T在containsAll和addAll兩個(gè)方法里面都只是用了一次。返
回類型并不依賴于類型參數(shù)或其他傳遞給該方法的實(shí)參(這種是只有一個(gè)實(shí)參的簡(jiǎn)單
情況)。這就告訴我們類型實(shí)參是用于多態(tài)的,它的作用只是對(duì)不同的調(diào)用可以有一
系列的實(shí)際的實(shí)參類型。如果是那樣的話,就應(yīng)該使用通配符,通配符就是設(shè)計(jì)來(lái)支
持靈活的子類型的,這也是我們這里所要表述的東西。
泛型方法允許類型參數(shù)用于表述一個(gè)或多個(gè)的實(shí)參類型對(duì)方法或及其返回類型的
依賴關(guān)系。如果沒(méi)有那樣的一個(gè)依賴關(guān)系的話,泛型方法就不應(yīng)用使用。
也有可能是一前一后一起使用泛型方法和通配符的情況,下面是Collections.copy()
方法:

class Collections {
public static <T> void copy(List<T> dest, list< ? extends T> src) {...}
}

請(qǐng)注意這里兩個(gè)參數(shù)類型的依賴關(guān)系,任何要從源鏈表src復(fù)制過(guò)來(lái)的對(duì)象都必
須是對(duì)目標(biāo)鏈表dst元素可賦值的;所以我們可以不管src的元素類型是什么,只要
它是T類型的子類型。copy方法的方法頭表示了使用一個(gè)類型參數(shù),但是用通配符來(lái)
作為第二個(gè)參數(shù)的元素類型的依賴關(guān)系。
我們是可以用另外一種不用通配符來(lái)寫這個(gè)方法頭的辦法。

class Collections {
public static <T, S extends T>
vod copy(List<T> dest, List<S> src) { ...}
}

沒(méi)問(wèn)題,但是當(dāng)?shù)谝粋€(gè)類型參數(shù)用作dst的類型和批二個(gè)類型參數(shù)S的上界的
時(shí)候,S它本身在src類型里只能使用一次,沒(méi)有其他的東西依賴于它。這就意味
著我們可以用一個(gè)通配符來(lái)代替S了。使用通配符比聲明顯式的類型參數(shù)要來(lái)得清
晰和簡(jiǎn)單,因此在可能的話都優(yōu)先使用通配符。
當(dāng)通配符用于方法頭外部,作為成員變量、局部變量和數(shù)組的類型的時(shí)候,同
樣也有優(yōu)勢(shì)。請(qǐng)看下面的例子。
看回我們之前畫圖的那個(gè)問(wèn)題,現(xiàn)在我們想要保留一份畫圖請(qǐng)求的歷史記錄。
我們可以這樣來(lái)維護(hù)這份歷史記錄,在Shape類里用一個(gè)靜態(tài)的變量表示歷史記錄,
然后在drawAll()方法里面把傳遞的實(shí)參儲(chǔ)存到那歷史記錄變量里頭。

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è)俅瘟粢庖幌率褂妙愋蛥?shù)的命名慣例。當(dāng)沒(méi)有更精確的類型來(lái)
區(qū)分的時(shí)候,我們用T來(lái)表示類型,這是通常是在泛型方法里面的情況。如果有多
個(gè)類型參數(shù),我們可以用在字母表中與T相鄰的字母來(lái)表示,比如S。如果一個(gè)泛
型方法出現(xiàn)在一個(gè)泛型類里面,一個(gè)好的方法就是,應(yīng)該避免對(duì)方法和類使用相
同的類型參數(shù)以免發(fā)生混淆。這在嵌套泛型類里也一樣。
-----------------------------------------------------------------------------------------------
六、與遺留代碼的交互


到現(xiàn)在為止,我們所有的例子都是在一個(gè)假想的理想世界里面的,就是所有的
人都在使用Java語(yǔ)言支持泛型的最新版本。
唉,不過(guò)在現(xiàn)實(shí)中情況卻不是那樣。千百萬(wàn)行的代碼都是用早期版本的語(yǔ)言
來(lái)編寫的,不可能把它們?nèi)吭谝灰怪g就轉(zhuǎn)換過(guò)來(lái)。
在后面的第10部分,我們將會(huì)解決把遺留代碼轉(zhuǎn)為用泛型這個(gè)問(wèn)題。在這部分
我們要看的是比較簡(jiǎn)單的問(wèn)題:遺留代碼與泛型代碼如何交互?這個(gè)問(wèn)題分為兩個(gè)
部分:在泛型代碼中使用遺留代碼和在遺留代碼中使用泛型代碼。
------------------------------------------------------------------------------------------------
六-1 在泛型代碼中使用遺留代碼

[url=http://xoj.blogone.net][url]
當(dāng)你在享受在代碼中使用泛型帶來(lái)的好處的時(shí)候,你怎么樣使用遺留代碼呢?
假設(shè)這樣一個(gè)例子,你要使用com.Foodlibar.widgets這個(gè)包。Fooblibar.com
的人要銷售一個(gè)庫(kù)存控制系統(tǒng),主要部分如下:

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
}

現(xiàn)在,你可以用上面的API來(lái)增加新的代碼,它可以很好的保證你調(diào)用參數(shù)恰當(dāng)
的addAssembly()方法,就是說(shuō)傳遞的集合是一個(gè)Part類型的Collection對(duì)象,當(dāng)
然,泛型是最適合做這個(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(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();
}
}

當(dāng)我們調(diào)用addAssembly方法的時(shí)候,它想要的第二個(gè)參數(shù)是Collection類型的,
實(shí)參是Collection<Part>類型,但卻可以,為什么呢?畢竟,大多數(shù)集合存儲(chǔ)的都不是
Part對(duì)象,所以總的來(lái)說(shuō),編譯器不會(huì)知道Collection存儲(chǔ)的是什么類型的集合。
在正規(guī)的泛型代碼里面,Collection都帶有類型參數(shù)。當(dāng)一個(gè)像Collection這樣
的泛型不帶類型參數(shù)使用的時(shí)候,稱之為原生類型。
很多人的第一直覺(jué)是Collection就是指Collection<Object>,但從我們先前所
看到的可以知道,當(dāng)需要的對(duì)象是Collection<Object>,而傳遞的卻是Collection<Part>
對(duì)象的時(shí)候,是類型不安全的。確切點(diǎn)的說(shuō)法是Collection類型表示一個(gè)未知類型的
集合,就像Collection<?>。
稍等一下,那樣做也是不正確的!考慮一下調(diào)用getParts()方法,它返回一個(gè)
Collection對(duì)象,然后賦值給k,而k是Collection<Part>類型的;如果調(diào)用的結(jié)果
是返回一個(gè)Collection<?>的對(duì)象,這個(gè)賦值可能是錯(cuò)誤的。
事實(shí)上,這個(gè)賦值是允許的,只是它會(huì)產(chǎn)生一個(gè)未檢測(cè)警告。警告是需要的,因?yàn)?br />編譯器不能保證賦值的正確性。我們沒(méi)有辦法通過(guò)檢測(cè)遺留代碼中的getAssembly()方法
來(lái)保證返回的集合的確是一個(gè)類型參數(shù)是Part的集合。程序里面的類型是Collection,
我們可以合法的對(duì)此集合插入任何對(duì)象。
所以,這不應(yīng)該是錯(cuò)誤的嗎?理論上來(lái)說(shuō),答案是:是;但實(shí)際上如果是泛型代碼
調(diào)用遺留代碼的話,這又是允許的。對(duì)這個(gè)賦值是否可接受,得取決于程序員自己,在
這個(gè)例子中賦值是安全的,因?yàn)間etAssembly()方法約定是返回以Part作為類型參數(shù)的
集合,盡管在類型標(biāo)記中沒(méi)有表明。
所以原生類型很像通配符類型,但它們沒(méi)有那么嚴(yán)格的類型檢測(cè)。這是有意設(shè)計(jì)成
這樣的,從而可以允許泛型代碼可以與之前已有的遺留代碼交互。
在泛型代碼中調(diào)用遺留代碼固然是危險(xiǎn)的,一旦把泛型代碼和非泛型代碼混合在一
起,泛型系統(tǒng)所提供的全部安全保證就都變得無(wú)效了。但這仍比根本不使用泛型要好,
最起碼你知道你的代碼是一致的。
泛型代碼出現(xiàn)的今天,仍然有很多非泛型代碼,二者混合同時(shí)使用是不可避免的。
如果一定要把遺留代碼與泛型代碼混合使用,請(qǐng)小心留意那些未檢測(cè)警告。仔細(xì)的
想想如何才能判定引發(fā)警告的代碼是安全的。
如果仍然出錯(cuò),代碼引發(fā)的警告實(shí)際不是類型安全的,那又怎么樣呢?我們會(huì)看
那樣的情況,接下來(lái),我們將會(huì)部分的觀察編譯器的工作方式。
--------------------------------------------------------------------------------------------------
六-2 擦除和翻譯


public String loophole(Integer x){
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x);//編譯時(shí)未檢測(cè)警告
return ys.iterator().next();
}

在這里我們定義了一個(gè)字符串類型的鏈表和一個(gè)一般的老式鏈表,我們先插入
一個(gè)Integer對(duì)象,然后試圖取出一個(gè)String對(duì)象,很明顯這是錯(cuò)誤的。如果我們
忽略警告繼續(xù)執(zhí)行代碼的話,程序?qū)?huì)在我們使用錯(cuò)誤類型的地方出錯(cuò)。在運(yùn)行時(shí),
代碼執(zhí)行大致如下:

public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return (String)ys.iterator().next();//運(yùn)行時(shí)出錯(cuò)
}

當(dāng)我們要從鏈表中取出一個(gè)元素,并把它當(dāng)作是一個(gè)字符串對(duì)象而把它轉(zhuǎn)換為
String類型的時(shí)候,我們將會(huì)得到一個(gè)ClassCastException類型轉(zhuǎn)換異常。在
泛型版本的loophole()方法里面發(fā)生的就是這種情況。
出現(xiàn)這種情況的原因是,Java的泛型是通過(guò)一個(gè)前臺(tái)轉(zhuǎn)換“擦除”的編譯器實(shí)現(xiàn)
的,你基本上可以認(rèn)為它是一個(gè)源碼對(duì)源碼的翻譯,這就是為何泛型版的loophole()
方法轉(zhuǎn)變?yōu)榉欠盒桶姹镜脑颉?br />結(jié)果是,Java虛擬機(jī)的類型安全性和完整性永遠(yuǎn)不會(huì)有問(wèn)題,就算出現(xiàn)未檢測(cè)
的警告。
基本上,擦除會(huì)除去所有的泛型信息。尖括號(hào)里面的所有類型信息都會(huì)去掉,比
如,參數(shù)化類型的List<String>會(huì)轉(zhuǎn)換為L(zhǎng)ist。類型變量在之后使用時(shí)會(huì)被類型
變量的上界(通常是Object)所替換。當(dāng)最后代碼不是類型正確的時(shí)候,就會(huì)加入
一個(gè)適當(dāng)?shù)念愋娃D(zhuǎn)換,就像loophole()方法的最后一行。
對(duì)“擦除”的完整描述不是本指南的范圍內(nèi)的內(nèi)容,但前面我們所給的簡(jiǎn)單描述
也差不多是那樣了。了解這點(diǎn)很有好處,特別是當(dāng)你想做諸如把現(xiàn)有API轉(zhuǎn)為使用
泛型(請(qǐng)看第10部分)這樣復(fù)雜的東西,或者是想知道為什么它們會(huì)那樣的時(shí)候。
-----------------------------------------------------------------------------------------------
六-3 在遺留代碼中使用泛型



現(xiàn)在我們來(lái)看看相反的情況。假設(shè)Fooblibar.com把他們的API轉(zhuǎn)換為泛型的,
但有些客戶還沒(méi)有轉(zhuǎn)換。代碼就會(huì)像下面的:

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();
}
}

客戶代碼是在引進(jìn)泛型之前寫下的,但是它使用了com.Fooblibar.widgets包和集
合庫(kù),兩個(gè)現(xiàn)在都是在用泛型的。在客戶代碼里面使用的泛型全部都是原生類型。
第1行產(chǎn)生一個(gè)未檢測(cè)警告,因?yàn)榘岩粋€(gè)原生Collection傳遞給了一個(gè)需要Part類型的
Collection的地方,編譯器不能保證原生的Collection是一個(gè)Part類型的Collection。
不這樣做的話,你也可以在編譯客戶代碼的時(shí)候使用source 1.4這個(gè)標(biāo)記來(lái)保證不
會(huì)產(chǎn)生警告。但是這樣的話你就不能使用所有JDK 1.5引入的新的語(yǔ)言特性。
-----------------------------------------------------------------------------------------------
七、晦澀難懂的部分

七-1 泛型類為所有調(diào)用所共享
下面的代碼段會(huì)打印出什么呢?

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)樗蟹盒皖惖膶?shí)例它們
的運(yùn)行時(shí)的類(run-time class)都是一樣的,不管它們實(shí)際類型參數(shù)如何。
泛型類之所以為泛型的,是因?yàn)樗鼘?duì)所有可能的類型參數(shù)都有相同的行為,相同
的類可以看作是有很多不同的類型。
結(jié)果就是,一個(gè)類的靜態(tài)的變量和方法也共享于所有的實(shí)例中,這就是為什么不
允許在靜態(tài)方法或初始化部分、或者在靜態(tài)變量的聲明或初始化中引用類型參數(shù)。
---------------------------------------------------------------------------------------------
七-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.