什么是泛型癣疟?
泛型(Generic type 或者 generics)是對?Java?語言的類型系統(tǒng)的一種擴(kuò)展个唧,以支持創(chuàng)建可以按類型進(jìn)行參數(shù)化的類婉称。可以把類型參數(shù)看作是使用參數(shù)化類型時(shí)指定的類型的一個(gè)占位符贤牛,就像方法的形式參數(shù)是運(yùn)行時(shí)傳遞的值的占位符一樣惋鹅。
可以在集合框架(Collection framework)中看到泛型的動機(jī)。例如殉簸,Map 類允許您向一個(gè) Map 添加任意類的對象闰集,即使最常見的情況是在給定映射(map)中保存某個(gè)特定類型(比如 String)的對象。
因?yàn)?Map.get() 被定義為返回 Object般卑,所以一般必須將 Map.get() 的結(jié)果強(qiáng)制類型轉(zhuǎn)換為期望的類型武鲁,如下面的代碼所示:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
要讓程序通過編譯,必須將 get() 的結(jié)果強(qiáng)制類型轉(zhuǎn)換為 String蝠检,并且希望結(jié)果真的是一個(gè) String沐鼠。但是有可能某人已經(jīng)在該映射中保存了不是 String 的東西,這樣的話叹谁,上面的代碼將會拋出 ClassCastException饲梭。
理想情況下,您可能會得出這樣一個(gè)觀點(diǎn)焰檩,即 m 是一個(gè) Map憔涉,它將 String 鍵映射到 String 值。這可以讓您消除代碼中的強(qiáng)制類型轉(zhuǎn)換析苫,同時(shí)獲得一個(gè)附加的類型檢查層兜叨,該檢查層可以防止有人將錯誤類型的鍵或值保存在集合中穿扳。這就是泛型所做的工作。
泛型的好處
Java?語言中引入泛型是一個(gè)較大的功能增強(qiáng)国旷。不僅語言纵揍、類型系統(tǒng)和編譯器有了較大的變化,以支持泛型议街,而且類庫也進(jìn)行了大翻修,所以許多重要的類璧榄,比如集合框架特漩,都已經(jīng)成為泛型化的了。這帶來了很多好處:
類型安全骨杂。?泛型的主要目標(biāo)是提高?Java?程序的類型安全涂身。通過知道使用泛型定義的變量的類型限制,編譯器可以在一個(gè)高得多的程度上驗(yàn)證類型假設(shè)搓蚪。沒有泛型,這些假設(shè)就只存在于程序員的頭腦中(或者如果幸運(yùn)的話,還存在于代碼注釋中)胆敞。
Java?程序中的一種流行技術(shù)是定義這樣的集合旁振,即它的元素或鍵是公共類型的,比如“String 列表”或者“String 到 String 的映射”雳灾。通過在變量聲明中捕獲這一附加的類型信息漠酿,泛型允許編譯器實(shí)施這些附加的類型約束。類型錯誤現(xiàn)在就可以在編譯時(shí)被捕獲了谎亩,而不是在運(yùn)行時(shí)當(dāng)作 ClassCastException 展示出來炒嘲。將類型檢查從運(yùn)行時(shí)挪到編譯時(shí)有助于您更容易找到錯誤,并可提高程序的可靠性匈庭。
消除強(qiáng)制類型轉(zhuǎn)換夫凸。?泛型的一個(gè)附帶好處是,消除源代碼中的許多強(qiáng)制類型轉(zhuǎn)換阱持。這使得代碼更加可讀夭拌,并且減少了出錯機(jī)會。
盡管減少強(qiáng)制類型轉(zhuǎn)換可以降低使用泛型類的代碼的羅嗦程度衷咽,但是聲明泛型變量會帶來相應(yīng)的羅嗦啼止。比較下面兩個(gè)代碼例子。
該代碼不使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
該代碼使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = li.get(0);
在簡單的程序中使用一次泛型變量不會降低羅嗦程度兵罢。但是對于多次使用泛型變量的大型程序來說献烦,則可以累積起來降低羅嗦程度。
潛在的性能收益卖词。?泛型為較大的優(yōu)化帶來可能巩那。在泛型的初始實(shí)現(xiàn)中吏夯,編譯器將強(qiáng)制類型轉(zhuǎn)換(沒有泛型的話,程序員會指定這些強(qiáng)制類型轉(zhuǎn)換)插入生成的字節(jié)碼中即横。但是更多類型信息可用于編譯器這一事實(shí)噪生,為未來版本的 JVM 的優(yōu)化帶來可能。
由于泛型的實(shí)現(xiàn)方式东囚,支持泛型(幾乎)不需要 JVM 或類文件更改跺嗽。所有工作都在編譯器中完成,編譯器生成類似于沒有泛型(和強(qiáng)制類型轉(zhuǎn)換)時(shí)所寫的代碼页藻,只是更能確保類型安全而已桨嫁。
泛型用法的例子
泛型的許多最佳例子都來自集合框架,因?yàn)?b>泛型讓您在保存在集合中的元素上指定類型約束份帐×О桑考慮這個(gè)使用 Map 類的例子,其中涉及一定程度的優(yōu)化废境,即 Map.get() 返回的結(jié)果將確實(shí)是一個(gè) String:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
如果有人已經(jīng)在映射中放置了不是 String 的其他東西畜挨,上面的代碼將會拋出 ClassCastException。泛型允許您表達(dá)這樣的類型約束噩凹,即 m 是一個(gè)將 String 鍵映射到 String 值的 Map巴元。這可以消除代碼中的強(qiáng)制類型轉(zhuǎn)換,同時(shí)獲得一個(gè)附加的類型檢查層驮宴,這個(gè)檢查層可以防止有人將錯誤類型的鍵或值保存在集合中务冕。
下面的代碼示例展示了 JDK 5.0 中集合框架中的 Map 接口的定義的一部分:
public interface Map {
public void put(K key, V value);
public V get(K key);
}
注意該接口的兩個(gè)附加物:
類型參數(shù) K 和 V 在類級別的規(guī)格說明,表示在聲明一個(gè) Map 類型的變量時(shí)指定的類型的占位符幻赚。
在 get()禀忆、put() 和其他方法的方法簽名中使用的 K 和 V。
為了贏得使用泛型的好處落恼,必須在定義或?qū)嵗?Map 類型的變量時(shí)為 K 和 V 提供具體的值箩退。以一種相對直觀的方式做這件事:
Map m = new HashMap();
m.put("key", "blarg");
String s = m.get("key");
當(dāng)使用 Map 的泛型化版本時(shí),您不再需要將 Map.get() 的結(jié)果強(qiáng)制類型轉(zhuǎn)換為 String佳谦,因?yàn)榫幾g器知道 get() 將返回一個(gè) String戴涝。
在使用泛型的版本中并沒有減少鍵盤錄入;實(shí)際上钻蔑,比使用強(qiáng)制類型轉(zhuǎn)換的版本需要做更多鍵入啥刻。使用泛型只是帶來了附加的類型安全。因?yàn)榫幾g器知道關(guān)于您將放進(jìn) Map 中的鍵和值的類型的更多信息咪笑,所以類型檢查從執(zhí)行時(shí)挪到了編譯時(shí)可帽,這會提高可靠性并加快開發(fā)速度。
向后兼容
在?Java?語言中引入泛型的一個(gè)重要目標(biāo)就是維護(hù)向后兼容窗怒。盡管 JDK 5.0 的標(biāo)準(zhǔn)類庫中的許多類映跟,比如集合框架蓄拣,都已經(jīng)泛型化了,但是使用集合類(比如 HashMap 和 ArrayList)的現(xiàn)有代碼將繼續(xù)不加修改地在 JDK 5.0 中工作努隙。當(dāng)然球恤,沒有利用泛型的現(xiàn)有代碼將不會贏得泛型的類型安全好處。
二?泛型基礎(chǔ)
類型參數(shù)
在定義泛型類或聲明泛型類的變量時(shí)荸镊,使用尖括號來指定形式類型參數(shù)咽斧。形式類型參數(shù)與實(shí)際類型參數(shù)之間的關(guān)系類似于形式方法參數(shù)與實(shí)際方法參數(shù)之間的關(guān)系,只是類型參數(shù)表示類型躬存,而不是表示值张惹。
泛型類中的類型參數(shù)幾乎可以用于任何可以使用類名的地方。例如优构,下面是?java.util.Map 接口的定義的摘錄:
public interface Map {
public void put(K key, V value);
public V get(K key);
}
Map 接口是由兩個(gè)類型參數(shù)化的,這兩個(gè)類型是鍵類型 K 和值類型 V雁竞。(不使用泛型)將會接受或返回 Object 的方法現(xiàn)在在它們的方法簽名中使用 K 或 V钦椭,指示附加的類型約束位于 Map 的規(guī)格說明之下。
當(dāng)聲明或者實(shí)例化一個(gè)泛型的對象時(shí)碑诉,必須指定類型參數(shù)的值:
Map<String, String> map = new HashMap<String, String>();
注意彪腔,在本例中,必須指定兩次類型參數(shù)进栽。一次是在聲明變量 map 的類型時(shí)德挣,另一次是在選擇 HashMap 類的參數(shù)化以便可以實(shí)例化正確類型的一個(gè)實(shí)例時(shí)。
編譯器在遇到一個(gè) Map<String, String> 類型的變量時(shí)快毛,知道 K 和 V 現(xiàn)在被綁定為 String格嗅,因此它知道在這樣的變量上調(diào)用 Map.get() 將會得到 String 類型。
除了異常類型唠帝、枚舉或匿名內(nèi)部類以外屯掖,任何類都可以具有類型參數(shù)。
命名類型參數(shù)
推薦的命名約定是使用大寫的單個(gè)字母名稱作為類型參數(shù)襟衰。這與 C++ 約定有所不同(參閱 附錄 A:與 C++ 模板的比較)贴铜,并反映了大多數(shù)泛型類將具有少量類型參數(shù)的假定。對于常見的泛型模式瀑晒,推薦的名稱是:
K —— 鍵绍坝,比如映射的鍵。
V —— 值苔悦,比如 List 和 Set 的內(nèi)容轩褐,或者 Map 中的值。
E —— 異常類玖详。
T ——?泛型灾挨。
泛型不是協(xié)變的
關(guān)于泛型的混淆邑退,一個(gè)常見的來源就是假設(shè)它們像數(shù)組一樣是協(xié)變的。其實(shí)它們不是協(xié)變的劳澄。List 不是 List 的父類型地技。
如果 A 擴(kuò)展 B,那么 A 的數(shù)組也是 B 的數(shù)組秒拔,并且完全可以在需要 B[] 的地方使用 A[]:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代碼是有效的,因?yàn)橐粋€(gè) Integer 是 一個(gè) Number砂缩,因而一個(gè) Integer 數(shù)組是 一個(gè) Number 數(shù)組作谚。但是對于泛型來說則不然。下面的代碼是無效的:
List intList = new ArrayList();
List numberList = intList; // invalid
最初庵芭,大多數(shù)?Java?程序員覺得這缺少協(xié)變很煩人妹懒,或者甚至是“壞的(broken)”,但是之所以這樣有一個(gè)很好的原因双吆。如果可以將 List 賦給 List眨唬,下面的代碼就會違背泛型應(yīng)該提供的類型安全:
List intList = new ArrayList();
List numberList = intList; // invalid
numberList.add(new Float(3.1415));
因?yàn)?intList 和 numberList 都是有別名的,如果允許的話好乐,上面的代碼就會讓您將不是 Integers 的東西放進(jìn) intList 中匾竿。但是,正如下一屏將會看到的蔚万,您有一個(gè)更加靈活的方式來定義泛型岭妖。
類型通配符
假設(shè)您具有該方法:
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
上面的代碼在 JDK 5.0 上編譯通過,但是如果試圖用 List 調(diào)用它反璃,則會得到警告昵慌。出現(xiàn)警告是因?yàn)椋鷮?b>泛型(List)傳遞給一個(gè)只承諾將它當(dāng)作 List(所謂的原始類型)的方法淮蜈,這將破壞使用泛型的類型安全废离。
如果試圖編寫像下面這樣的方法,那么將會怎么樣礁芦?
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
它仍然不會通過編譯蜻韭,因?yàn)橐粋€(gè) List 不是 一個(gè) List(正如前一屏?泛型不是協(xié)變的 中所學(xué)的)。這才真正煩人 —— 現(xiàn)在您的泛型版本還沒有普通的非泛型版本有用柿扣!
解決方案是使用類型通配符:
void printList(List l) {
for (Object o : l)
System.out.println(o);
}
上面代碼中的問號是一個(gè)類型通配符肖方。它讀作“問號”。List 是任何泛型?List 的父類型未状,所以您完全可以將 List俯画、List 或 List>> 傳遞給 printList()。
類型通配符的作用
前一屏 類型通配符 中引入了類型通配符司草,這讓您可以聲明 List<?> 類型的變量艰垂。您可以對這樣的 List 做什么呢泡仗?非常方便,可以從中檢索元素猜憎,但是不能添加元素娩怎。原因不是編譯器知道哪些方法修改列表哪些方法不修改列表,而是(大多數(shù))變化的方法比不變化的方法需要更多的類型信息胰柑。下面的代碼則工作得很好:
List li = new ArrayList();
li.add(new Integer(42));
List lu = li;
System.out.println(lu.get(0));
為什么該代碼能工作呢截亦?對于 lu,編譯器一點(diǎn)都不知道 List 的類型參數(shù)的值柬讨。但是編譯器比較聰明崩瓤,它可以做一些類型推理。在本例中踩官,它推斷未知的類型參數(shù)必須擴(kuò)展 Object却桶。(這個(gè)特定的推理沒有太大的跳躍,但是編譯器可以作出一些非常令人佩服的類型推理蔗牡,后面就會看到(在 底層細(xì)節(jié) 一節(jié)中)颖系。所以它讓您調(diào)用 List.get() 并推斷返回類型為 Object。
另一方面蛋逾,下面的代碼不能工作:
List li = new ArrayList();
li.add(new Integer(42));
List lu = li;
lu.add(new Integer(43)); // error
在本例中集晚,對于 lu窗悯,編譯器不能對 List 的類型參數(shù)作出足夠嚴(yán)密的推理区匣,以確定將 Integer 傳遞給 List.add() 是類型安全的。所以編譯器將不允許您這么做蒋院。
以免您仍然認(rèn)為編譯器知道哪些方法更改列表的內(nèi)容哪些不更改列表內(nèi)容亏钩,請注意下面的代碼將能工作,因?yàn)樗灰蕾囉诰幾g器必須知道關(guān)于 lu 的類型參數(shù)的任何信息:
List li = new ArrayList();
li.add(new Integer(42));
List lu = li;
lu.clear();
泛型方法
(在 類型參數(shù) 一節(jié)中)您已經(jīng)看到欺旧,通過在類的定義中添加一個(gè)形式類型參數(shù)列表姑丑,可以將類泛型化。方法也可以被泛型化辞友,不管它們定義在其中的類是不是泛型化的栅哀。
泛型類在多個(gè)方法簽名間實(shí)施類型約束。在 List 中称龙,類型參數(shù) V 出現(xiàn)在 get()留拾、add()、contains() 等方法的簽名中鲫尊。當(dāng)創(chuàng)建一個(gè) Map 類型的變量時(shí)痴柔,您就在方法之間宣稱一個(gè)類型約束。您傳遞給 add() 的值將與 get() 返回的值的類型相同疫向。
類似地咳蔚,之所以聲明泛型方法豪嚎,一般是因?yàn)槟胍谠摲椒ǖ亩鄠€(gè)參數(shù)之間宣稱一個(gè)類型約束。例如谈火,下面代碼中的 ifThenElse() 方法侈询,根據(jù)它的第一個(gè)參數(shù)的布爾值,它將返回第二個(gè)或第三個(gè)參數(shù):
public T ifThenElse(boolean b, T first, T second) {
return b ? first : second;
}
注意堆巧,您可以調(diào)用 ifThenElse()妄荔,而不用顯式地告訴編譯器,您想要 T 的什么值谍肤。編譯器不必顯式地被告知 T 將具有什么值啦租;它只知道這些值都必須相同。編譯器允許您調(diào)用下面的代碼荒揣,因?yàn)榫幾g器可以使用類型推理來推斷出篷角,替代 T 的 String 滿足所有的類型約束:
String s = ifThenElse(b, "a", "b");
類似地,您可以調(diào)用:
Integer i = ifThenElse(b, new Integer(1), new Integer(2));
但是系任,編譯器不允許下面的代碼恳蹲,因?yàn)闆]有類型會滿足所需的類型約束:
String s = ifThenElse(b, "pi", new Float(3.14));
為什么您選擇使用泛型方法,而不是將類型 T 添加到類定義呢俩滥?(至少)有兩種情況應(yīng)該這樣做:
當(dāng)泛型方法是靜態(tài)的時(shí)嘉蕾,這種情況下不能使用類類型參數(shù)。
當(dāng) T 上的類型約束對于方法真正是局部的時(shí)霜旧,這意味著沒有在相同類的另一個(gè) 方法簽名中使用相同 類型 T 的約束错忱。通過使得泛型方法的類型參數(shù)對于方法是局部的,可以簡化封閉類型的簽名挂据。
有限制類型
在前一屏?泛型方法 的例子中以清,類型參數(shù) V 是無約束的或無限制的 類型。有時(shí)在還沒有完全指定類型參數(shù)時(shí)崎逃,需要對類型參數(shù)指定附加的約束掷倔。
考慮例子 Matrix 類,它使用類型參數(shù) V个绍,該參數(shù)由 Number 類來限制:
public class Matrix<V extends Number> { ... }
編譯器允許您創(chuàng)建 Matrix 或 Matrix 類型的變量勒葱,但是如果您試圖定義 Matrix 類型的變量,則會出現(xiàn)錯誤巴柿。類型參數(shù) V 被判斷為由 Number 限制 凛虽。在沒有類型限制時(shí),假設(shè)類型參數(shù)由 Object 限制篮洁。這就是為什么前一屏?泛型方法 中的例子涩维,允許 List.get() 在 List 上調(diào)用時(shí)返回 Object,即使編譯器不知道類型參數(shù) V 的類型。
三 一個(gè)簡單的泛型類
編寫基本的容器類
此時(shí)瓦阐,您可以開始編寫簡單的泛型類了蜗侈。到目前為止,泛型類最常見的用例是容器類(比如集合框架)或者值持有者類(比如 WeakReference 或 ThreadLocal)睡蟋。我們來編寫一個(gè)類踏幻,它類似于 List,充當(dāng)一個(gè)容器戳杀。其中该面,我們使用泛型來表示這樣一個(gè)約束,即 Lhist 的所有元素將具有相同類型信卡。為了實(shí)現(xiàn)起來簡單隔缀,Lhist 使用一個(gè)固定大小的數(shù)組來保存值,并且不接受 null 值傍菇。
Lhist 類將具有一個(gè)類型參數(shù) V(該參數(shù)是 Lhist 中的值的類型)猾瘸,并將具有以下方法:
public class Lhist {
public Lhist(int capacity) { ... }
public int size() { ... }
public void add(V value) { ... }
public void remove(V value) { ... }
public V get(int index) { ... }
}
要實(shí)例化 Lhist,只要在聲明時(shí)指定類型參數(shù)和想要的容量:
Lhist<String> stringList = new Lhist<String>(10);
實(shí)現(xiàn)構(gòu)造函數(shù)
在實(shí)現(xiàn) Lhist 類時(shí)丢习,您將會遇到的第一個(gè)攔路石是實(shí)現(xiàn)構(gòu)造函數(shù)牵触。您可能會像下面這樣實(shí)現(xiàn)它:
public class Lhist {
private V[] array;
public Lhist(int capacity) {
array = new V[capacity]; // illegal
}
}
這似乎是分配后備數(shù)組最自然的一種方式,但是不幸的是咐低,您不能這樣做揽思。具體原因很復(fù)雜,當(dāng)學(xué)習(xí)到 底層細(xì)節(jié) 一節(jié)中的“擦除”主題時(shí)见擦,您就會明白钉汗。分配后備數(shù)組的實(shí)現(xiàn)方式很古怪且違反直覺。下面是構(gòu)造函數(shù)的一種可能的實(shí)現(xiàn)(該實(shí)現(xiàn)使用集合類所采用的方法):
public class Lhist {
private V[] array;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
}
另外锡宋,也可以使用反射來實(shí)例化數(shù)組儡湾。但是這樣做需要給構(gòu)造函數(shù)傳遞一個(gè)附加的參數(shù) —— 一個(gè)類常量特恬,比如 Foo.class执俩。后面在 Class 一節(jié)中將討論類常量。
實(shí)現(xiàn)方法
實(shí)現(xiàn) Lhist 的方法要容易得多癌刽。下面是 Lhist 類的完整實(shí)現(xiàn):
public class Lhist {
private V[] array;
private int size;
public Lhist(int capacity) {
array = (V[]) new Object[capacity];
}
public void add(V value) {
if (size == array.length)
throw new IndexOutOfBoundsException(Integer.toString(size));
else if (value == null)
throw new NullPointerException();
array[size++] = value;
}
public void remove(V value) {
int removalCount = 0;
for (int i=0; i
if (array[i].equals(value))
++removalCount;
else if (removalCount > 0) {
array[i-removalCount] = array[i];
array[i] = null;
}
}
size -= removalCount;
}
??? public int size() { return size; }
public V get(int i) {
if (i >= size)
throw new IndexOutOfBoundsException(Integer.toString(i));
return array[i];
}
}
注意役首,您在將會接受或返回 V 的方法中使用了形式類型參數(shù) V,但是您一點(diǎn)也不知道 V 具有什么樣的方法或域显拜,因?yàn)檫@些對泛型代碼是不可知的衡奥。
使用 Lhist 類
使用 Lhist 類很容易。要定義一個(gè)整數(shù) Lhist远荠,只需要在聲明和構(gòu)造函數(shù)中為類型參數(shù)提供一個(gè)實(shí)際值即可:
Lhist<Integer> li = new Lhist<Integer>(30);
編譯器知道矮固,li.get() 返回的任何值都將是 Integer 類型,并且它還強(qiáng)制傳遞給 li.add() 或 li.remove() 的任何東西都是 Integer。除了實(shí)現(xiàn)構(gòu)造函數(shù)的方式很古怪之外档址,您不需要做任何十分特殊的事情以使 Lhist 是一個(gè)泛型類盹兢。
四?Java類庫中的泛型
集合類
到目前為止,Java?類庫中泛型支持存在最多的地方就是集合框架守伸。就像容器類是 C++ 語言中模板的主要動機(jī)一樣(參閱 附錄 A:與 C++ 模板的比較)(盡管它們隨后用于很多別的用途)绎秒,改善集合類的類型安全是?Java?語言中泛型的主要動機(jī)。集合類也充當(dāng)如何使用泛型的模型尼摹,因?yàn)樗鼈冄菔玖?b>泛型的幾乎所有的標(biāo)準(zhǔn)技巧和方言见芹。
所有的標(biāo)準(zhǔn)集合接口都是泛型化的 —— Collection、List蠢涝、Set 和 Map玄呛。類似地,集合接口的實(shí)現(xiàn)都是用相同類型參數(shù)泛型化的和二,所以 HashMap 實(shí)現(xiàn) Map 等把鉴。
集合類也使用泛型的許多“技巧”和方言,比如上限通配符和下限通配符儿咱。例如庭砍,在接口 Collection 中,addAll 方法是像下面這樣定義的:
interface Collection {
boolean addAll(Collection);
}
該定義組合了通配符類型參數(shù)和有限制類型參數(shù)混埠,允許您將 Collection<Integer> 的內(nèi)容添加到 Collection<Number>怠缸。
如果類庫將 addAll() 定義為接受 Collection<V>,您就不能將 Collection<Integer> 的內(nèi)容添加到 Collection<Number>钳宪。不是限制 addAll() 的參數(shù)是一個(gè)與您將要添加到的集合包含相同類型的集合揭北,而有可能建立一個(gè)更合理的約束,即傳遞給 addAll() 的集合的元素 適合于添加到您的集合吏颖。有限制類型允許您這樣做搔体,并且使用有限制通配符使您不需要使用另一個(gè)不會用在其他任何地方的占位符名稱。
應(yīng)該可以將 addAll() 的類型參數(shù)定義為 Collection半醉。但是疚俱,這不但沒什么用,而且還會改變 Collection 接口的語義缩多,因?yàn)?b>泛型版本的語義將會不同于非泛型版本的語義呆奕。這闡述了泛型化一個(gè)現(xiàn)有的類要比定義一個(gè)新的泛型類難得多,因?yàn)槟仨氉⒁獠灰念惖恼Z義或者破壞現(xiàn)有的非泛型代碼衬吆。
作為泛型化一個(gè)類(如果不小心的話)如何會更改其語義的一個(gè)更加微妙的例子梁钾,注意 Collection.removeAll() 的參數(shù)的類型是 Collection,而不是 Collection逊抡。這是因?yàn)閭鬟f混合類型的集合給 removeAll() 是可接受的姆泻,并且更加限制地定義 removeAll 將會更改方法的語義和有用性。
其他容器類
除了集合類之外,Java?類庫中還有幾個(gè)其他的類也充當(dāng)值的容器拇勃。這些類包括 WeakReference蛾娶、SoftReference 和 ThreadLocal。它們都已經(jīng)在其包含的值的類型上泛型化了潜秋,所以 WeakReference 是對 T 類型的對象的弱引用蛔琅,ThreadLocal 則是到 T 類型的線程局部變量的句柄。
泛型不止用于容器
泛型最常見最直觀的使用是容器類峻呛,比如集合類或引用類(比如 WeakReference)罗售。Collection 中類型參數(shù)的含義很明顯 —— “一個(gè)所有值都是 V 類型的集合”。類似地钩述,ThreadLocal 也有一個(gè)明顯的解釋 —— “一個(gè)其類型是 T 的線程局部變量”寨躁。但是,泛型規(guī)格說明中沒有指定容積牙勘。
像 Comparable<T> 或 Class<T> 這樣的類中類型參數(shù)的含義更加微妙职恳。有時(shí),就像 Class<T> 中一樣方面,類型變量主要是幫助編譯器進(jìn)行類型推理放钦。有時(shí),就像隱含的 Enum<E extends Enum<E>> 中一樣恭金,類型變量只是在類層次結(jié)構(gòu)上加一個(gè)約束操禀。
Comparable
Comparable 接口已經(jīng)泛型化了,所以實(shí)現(xiàn) Comparable 的對象聲明它可以與什么類型進(jìn)行比較横腿。(通常颓屑,這是對象本身的類型,但是有時(shí)也可能是父類耿焊。)
public interface Comparable {
public boolean compareTo(T other);
}
所以 Comparable 接口包含一個(gè)類型參數(shù) T揪惦,該參數(shù)是一個(gè)實(shí)現(xiàn) Comparable 的類可以與之比較的對象的類型。這意味著如果定義一個(gè)實(shí)現(xiàn) Comparable 的類罗侯,比如 String器腋,就必須不僅聲明類支持比較,還要聲明它可與什么比較(通常是與它本身比較):
public class String implements Comparable<String> { ... }
現(xiàn)在來考慮一個(gè)二元 max() 方法的實(shí)現(xiàn)歇父。您想要接受兩個(gè)相同類型的參數(shù)蒂培,二者都是 Comparable再愈,并且相互之間是 Comparable榜苫。幸運(yùn)的是,如果使用泛型方法和有限制類型參數(shù)的話翎冲,這相當(dāng)直觀:
public static > T max(T t1, T t2) {
if (t1.compareTo(t2) > 0)
return t1;
else
return t2;
}
在本例中垂睬,您定義了一個(gè)泛型方法,在類型 T 上泛型化,您約束該類型擴(kuò)展(實(shí)現(xiàn)) Comparable驹饺。兩個(gè)參數(shù)都必須是 T 類型钳枕,這表示它們是相同類型,支持比較赏壹,并且相互可比較鱼炒。容易!
更好的是蝌借,編譯器將使用類型推理來確定當(dāng)調(diào)用 max() 時(shí) T 的值表示什么意思昔瞧。所以根本不用指定 T,下面的調(diào)用就能工作:
String s = max("moo", "bark");
編譯器將計(jì)算出 T 的預(yù)定值是 String菩佑,因此它將進(jìn)行編譯和類型檢查自晰。但是如果您試圖用不實(shí)現(xiàn) Comparable<X> 的 類 X 的參數(shù)調(diào)用 max(),那么編譯器將不允許這樣做稍坯。
Class<T>
類 Class 已經(jīng)泛型化了酬荞,但是很多人一開始都感覺其泛型化的方式很混亂。Class 中類型參數(shù) T 的含義是什么瞧哟?事實(shí)證明它是所引用的類接口器净。怎么會是這樣的呢?那是一個(gè)循環(huán)推理忍宋?如果不是的話鳖轰,為什么這樣定義它?
在以前的 JDK 中雄可,Class.newInstance() 方法的定義返回 Object凿傅,您很可能要將該返回類型強(qiáng)制轉(zhuǎn)換為另一種類型:
class Class {
Object newInstance();
}
但是使用泛型,您定義 Class.newInstance() 方法具有一個(gè)更加特定的返回類型:
class Class {
T newInstance();
}
如何創(chuàng)建一個(gè) Class 類型的實(shí)例数苫?就像使用非泛型代碼一樣聪舒,有兩種方式:調(diào)用方法 Class.forName() 或者使用類常量 X.class。Class.forName() 被定義為返回 Class虐急。另一方面箱残,類常量 X.class 被定義為具有類型 Class,所以 String.class 是 Class 類型的止吁。
讓 Foo.class 是 Class<Foo> 類型的有什么好處被辑?大的好處是,通過類型推理的魔力敬惦,可以提高使用反射的代碼的類型安全盼理。另外,還不需要將 Foo.class.newInstance() 強(qiáng)制類型轉(zhuǎn)換為 Foo俄删。
考慮一個(gè)方法宏怔,它從數(shù)據(jù)庫檢索一組對象奏路,并返回 JavaBeans 對象的一個(gè)集合。您通過反射來實(shí)例化和初始化創(chuàng)建的對象臊诊,但是這并不意味著類型安全必須完全被拋至腦后鸽粉。考慮下面這個(gè)方法:
public static List getRecords(Class c, Selector s) {
// Use Selector to select rows
List list = new ArrayList();
for (/* iterate over results */) {
T row = c.newInstance();
// use reflection to set fields from result
list.add(row);
}
return list;
}
可以像下面這樣簡單地調(diào)用該方法:
List<FooRecord> l = getRecords(FooRecord.class, fooSelector);
編譯器將會根據(jù) FooRecord.class 是 Class<FooRecord> 類型的這一事實(shí)抓艳,推斷 getRecords() 的返回類型触机。您使用類常量來構(gòu)造新的實(shí)例并提供編譯器在類型檢查中要用到的類型信息。
用 Class<T> 替換 T[]
Collection 接口包含一個(gè)方法玷或,用于將集合的內(nèi)容復(fù)制到一個(gè)調(diào)用者指定類型的數(shù)組中:
public Object[] toArray(Object[] prototypeArray) { ... }
toArray(Object[]) 的語義是威兜,如果傳遞的數(shù)組足夠大,就會使用它來保存結(jié)果庐椒,否則椒舵,就會使用反射分配一個(gè)相同類型的新數(shù)組。一般來說约谈,單獨(dú)傳遞一個(gè)數(shù)組作為參數(shù)來提供想要的返回類型是一個(gè)小技巧笔宿,但是在引入泛型之前,這是與方法交流類型信息最方便的方式棱诱。
有了泛型泼橘,就可以用一種更加直觀的方式來做這件事。不像上面這樣定義 toArray()迈勋,泛型?toArray() 可能看起來像下面這樣:
public<T> T[] toArray(Class<T> returnType)
調(diào)用這樣一個(gè) toArray() 方法很簡單:
FooBar[] fba = something.toArray(FooBar.class);
Collection 接口還沒有改變?yōu)槭褂迷摷夹g(shù)炬灭,因?yàn)檫@會破壞許多現(xiàn)有的集合實(shí)現(xiàn)。但是如果使用泛型從新構(gòu)建 Collection靡菇,則當(dāng)然會使用該方言來指定它想要返回值是哪種類型重归。
Enum<E>
JDK 5.0 中?Java?語言另一個(gè)增加的特性是枚舉。當(dāng)您使用 enum 關(guān)鍵字聲明一個(gè)枚舉時(shí)厦凤,編譯器就會在內(nèi)部為您生成一個(gè)類鼻吮,用于擴(kuò)展 Enum 并為枚舉的每個(gè)值聲明靜態(tài)實(shí)例。所以如果您說:
public enum Suit {HEART, DIAMOND, CLUB, SPADE};
編譯器就會在內(nèi)部生成一個(gè)叫做 Suit 的類较鼓,該類擴(kuò)展?java.lang.Enum 并具有叫做 HEART椎木、DIAMOND、CLUB 和 SPADE 的常量(public static final)成員博烂,每個(gè)成員都是 Suit 類香椎。
與 Class 一樣,Enum 也是一個(gè)泛型類禽篱。但是與 Class 不同畜伐,它的簽名稍微更復(fù)雜一些:
class Enum<E extends Enum<E>> { . . . }
這究竟是什么意思?這難道不會導(dǎo)致無限遞歸谆级?
我們逐步來分析烤礁。類型參數(shù) E 用于 Enum 的各種方法中讼积,比如 compareTo() 或 getDeclaringClass()肥照。為了這些方法的類型安全脚仔,Enum 類必須在枚舉的類上泛型化。
所以 extends Enum<E> 部分如何理解舆绎?該部分又具有兩個(gè)部分鲤脏。第一部分指出,作為 Enum 的類型參數(shù)的類本身必須是 Enum 的子類型吕朵,所以您不能聲明一個(gè)類 X 擴(kuò)展 Enum<Integer>猎醇。第二部分指出,任何擴(kuò)展 Enum 的類必須傳遞它本身 作為類型參數(shù)努溃。您不能聲明 X 擴(kuò)展 Enum<Y>硫嘶,即使 Y 擴(kuò)展 Enum。
總之梧税,Enum 是一個(gè)參數(shù)化的類型沦疾,只可以為它的子類型實(shí)例化,并且這些子類型然后將根據(jù)子類型來繼承方法第队。幸運(yùn)的是哮塞,在 Enum 情況下,編譯器為您做這些工作凳谦,一切都很好忆畅。
與非泛型代碼相互操作
數(shù)百萬行現(xiàn)有代碼使用已經(jīng)泛型化的?Java?類庫中的類,比如集合框架尸执、Class 和 ThreadLocal家凯。JDK 5.0 中的改進(jìn)不要破壞所有這些代碼是很重要的,所以編譯器允許您在不指定其類型參數(shù)的情況下使用泛型類如失。
當(dāng)然肆饶,以“舊方式”做事沒有新方式安全,因?yàn)楹雎粤司幾g器準(zhǔn)備提供的類型安全岖常。如果您試圖將 List<String> 傳遞給一個(gè)接受 List 的方法驯镊,它將能夠工作,但是編譯器將會發(fā)出一個(gè)可能喪失類型安全的警告竭鞍,即所謂的“unchecked conversion(不檢查轉(zhuǎn)換)”警告板惑。
沒有類型參數(shù)的泛型,比如聲明為 List 類型而不是 List 類型的變量偎快,叫做原始類型冯乘。原始類型與參數(shù)化類型的任何實(shí)例化是賦值兼容的,但是這樣的賦值會生成 unchecked-conversion 警告晒夹。
為了消除一些 unchecked-conversion 警告裆馒,假設(shè)您不準(zhǔn)備泛型化所有的代碼姊氓,您可以使用通配符類型參數(shù)。使用 List 而不使用 List喷好。List 是原始類型翔横;List 是具有未知類型參數(shù)的泛型。編譯器將以不同的方式對待它們梗搅,并很可能發(fā)出更少的警告禾唁。
無論在哪種情況下,編譯器在生成字節(jié)碼時(shí)都會生成強(qiáng)制類型轉(zhuǎn)換无切,所以生成的字節(jié)碼在每種情況下都不會比沒有泛型時(shí)更不安全荡短。如果您設(shè)法通過使用原始類型或類文件來破壞類型安全,就會得到與不使用泛型時(shí)得到的相同的 ClassCastException 或 ArrayStoreException哆键。
已檢查集合
作為從原始集合類型遷移到泛型集合類型的幫助掘托,集合框架添加了一些新的集合包裝器,以便為一些類型安全 bug 提供早期警告籍嘹。就像 Collections.unmodifiableSet() 工廠方法用一個(gè)不允許任何修改的 Set 包裝一個(gè)現(xiàn)有 Set 一樣闪盔,Collections.checkedSet()(以及 checkedList() 和 checkedMap())工廠方法創(chuàng)建一個(gè)包裝器(或者視圖)類,以防止您將錯誤類型的變量放在集合中噩峦。
checkedXxx() 方法都接受一個(gè)類常量作為參數(shù)锭沟,所以它們可以(在運(yùn)行時(shí))檢查這些修改是允許的。典型的實(shí)現(xiàn)可能像下面這樣:
public class Collections {
public static Collection checkedCollection(Collection c, Class type ) {
return new CheckedCollection(c, type);
}
private static class CheckedCollection implements Collection {
private final Collection c;
private final Class type;
CheckedCollection(Collection c, Class type) {
this.c = c;
this.type = type;
}
public boolean add(E o) {
if (!type.isInstance(o))
throw new ClassCastException();
else
return c.add(o);
}
}
}