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