泛型類型的限制
讓我們先查閱一下 Tiger 和 JSR-14 中泛型類型的使用限制:
- 不應(yīng)在靜態(tài)成員中引用封閉類型參數(shù)。
- 不能用基本類型實(shí)例化泛型類型參數(shù)。
- 不能在數(shù)據(jù)類型轉(zhuǎn)換或
instanceof
操作中使用“外露”類型參數(shù)。 - 不能在
new
操作中使用“外露”類型參數(shù)。 - 不能在類定義的
implements
或extends
子句中使用“外露”類型參數(shù)。
為什么會有這些限制呢?這要?dú)w因于 Tiger 和 JSR-14 為在 JVM 上實(shí)現(xiàn)泛型類型所使用的機(jī)制。由于 JVM 根本不支持泛型類型,所以這些編譯器“耍了個花招”,使得似乎存在對泛型類型的支持 — 它們用泛型類型信息檢查所有的代碼,但隨即“擦除”所有的泛型類型并生成只包含普通類型的類文件。
例如,將象 List<T>
這樣的泛型類型擦除得只剩下 List
。“外露”類型參數(shù) — 單獨(dú)出現(xiàn)而不是位于某個類型中的類型參數(shù)(如類 List<T>
中的類型參數(shù) T
)— 被簡單地擦除成它們的上界(就 T
而言,其上界就是 Object
)。
這一技術(shù)的功能極其強(qiáng)大;我們可以使幾乎所有泛型類型的精度得到增強(qiáng),但又與 JVM 保持兼容。事實(shí)上,我們甚至可以交替地使用非泛型的舊類(比如 List
)和其對應(yīng)的泛型類(List<T>
);兩者在運(yùn)行時看起來是一樣的。
遺憾的是,正如以上的限制所示,獲得這一功能是有代價的。以這種方式進(jìn)行擦除在類型系統(tǒng)中引入了缺陷,這些缺陷限制我們使用泛型類型的安全性。
為了幫助說明每種限制,我們查閱會出現(xiàn)這些限制的示例。在本文中,我們將討論前三個限制。與后兩個限制有關(guān)的問題過于復(fù)雜,因而需要更深入的研究,留待下一篇文章討論。
靜態(tài)成員中的封閉類型參數(shù)
編譯器完全禁止在靜態(tài)方法和靜態(tài)內(nèi)部類中引用封閉類型參數(shù)。所以,舉例來說,以下代碼在 Tiger 中就是非法的:
|
當(dāng)編譯這一代碼時,會生成兩個錯誤:
- 在靜態(tài)方法
m
中非法引用T
的錯誤 - 在靜態(tài)類
D
中非法引用T
的錯誤
當(dāng)定義靜態(tài)字段時,情況變得更加復(fù)雜。在 JSR-14 和 Tiger 中,在泛型類的所有實(shí)例中共享該類中的靜態(tài)字段?,F(xiàn)在,在 JSR-14 編譯器 1.0 和 1.2 中,如果您在靜態(tài)字段聲明中引用類型參數(shù),編譯器不會報錯,但它本應(yīng)該這么做。字段被共享這一事實(shí)很容易在運(yùn)行時導(dǎo)致奇怪的錯誤,如在不包含數(shù)據(jù)類型轉(zhuǎn)換的代碼中出現(xiàn) ClassCastException
。
例如,以下程序?qū)⒃谶@兩個版本的 JSR-14 下通過編譯而沒有任何警告:
清單 2. 在靜態(tài)字段中對封閉類型參數(shù)的有問題的引用
|
請注意,每次分配類 C
的實(shí)例時,都要重新設(shè)置靜態(tài)字段 member
。而且,它被設(shè)置成的對象類型取決于 C
的實(shí)例的類型!在所提供的 main
方法中,第一個實(shí)例 c
是 C<String>
類型。而第二個是 C<Integer>
類型。每當(dāng)從 c
訪問 member
這一共享靜態(tài)字段時,總是假定 member
的類型是 String
。但是,在分配了類型為 C<Integer>
的第二個實(shí)例之后,member
的類型是 Integer
。
運(yùn)行 C
的 main
方法的結(jié)果可能會讓您吃驚 — 它將發(fā)出一個 ClassCastException
!源代碼根本沒有包含任何數(shù)據(jù)類型轉(zhuǎn)換,怎么會這樣呢?事實(shí)證明編譯器確實(shí)在編譯階段將數(shù)據(jù)類型轉(zhuǎn)換插入到代碼中,這樣做是為了解決類型擦除會降低某些表達(dá)式的類型的精度這一事實(shí)。這些數(shù)據(jù)類型轉(zhuǎn)換被期望能夠成功,但在本例中卻沒有成功。
應(yīng)該認(rèn)為 JSR-14 1.0 和 1.2 的這一特殊“功能”是個錯誤。它破壞了類型系統(tǒng)的健全性,或者可以說,它破壞了類型系統(tǒng)應(yīng)該和程序員達(dá)成的“基本契約”。象對靜態(tài)方法和類所做的那樣,只要防止程序員在靜態(tài)字段中引用泛型類型,情況就會好很多。
請注意允許這種有潛在“爆炸性”的代碼存在所帶來的問題并不是程序員有意在自己的代碼中覆蓋類型系統(tǒng)。問題是程序員可能會無意中編寫這樣的代碼(比如,由于“復(fù)制和粘貼”操作,錯誤地在字段聲明中包括靜態(tài)修飾符)。
類型檢查器應(yīng)該能幫助程序員從這些類型的錯誤中恢復(fù),但對于靜態(tài)字段而言,類型系統(tǒng)實(shí)際上會使程序員更迷惑。當(dāng)未使用數(shù)據(jù)類型轉(zhuǎn)換的代碼中顯示的唯一錯誤就是 ClassCastException
時,我們應(yīng)如何診斷這樣的錯誤?對于不清楚 Tiger 中泛型類型所用的實(shí)現(xiàn)方案而又恰好假定類型系統(tǒng)合理運(yùn)行的程序員而言,情況更糟。因?yàn)樵谶@樣的情況下,類型系統(tǒng)不是合理地運(yùn)行。
幸運(yùn)的是,JSR-14 的最新版本(1.3)宣布在靜態(tài)字段中使用類型參數(shù)是不合法的。因此,我們有理由期待在 Tiger 的靜態(tài)字段中使用類型參數(shù)也是不合法的。
泛型類型參數(shù)和基本類型
和我們剛才討論的不同,這一限制沒有同樣的潛在缺陷,但它會使您的代碼非常冗長。例如,在 java.util.Hashtable
的泛型版本中,有兩種類型參數(shù):用于 Key
類型的和用于 Value
類型的。因此,如果我們想要一個將 String
映射到 String
的 Hashtable
,我們可以用表達(dá)式 new Hashtable<String, String>()
指定新的實(shí)例。但是,如果我們想要一個將 String
映射到 int
的 Hashtable
,我們只能創(chuàng)建 Hashtable<String, Integer>
的實(shí)例,并將所有的 int
值包裝在 Integer
中。
同樣,Tiger 在這方面當(dāng)然也是由所用的實(shí)現(xiàn)方案得到的。既然類型參數(shù)被擦除為它們的界限,而界限不能是基本類型,所以一旦類型被擦除,則對基本類型的實(shí)例化會完全沒有意義。
數(shù)據(jù)類型轉(zhuǎn)換或 instanceof 操作中的“外露”參數(shù)
回想一下,對于“外露”類型參數(shù),我們是指在詞匯上單獨(dú)出現(xiàn)的類型參數(shù),而不是更大類型的語法子組件。例如,C<T>
不是“外露”類型參數(shù),但(在 C
主體中)T
是。
如果在代碼中對“外露”類型參數(shù)進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換或 instanceof
操作,則編譯器將發(fā)出名為“unchecked”的警告。例如,以下代碼將生成警告:Warning: unchecked cast to type T
:
|
您應(yīng)該嚴(yán)肅地對待這些警告,因?yàn)樗鼈冋f明您的代碼在運(yùn)行時會表現(xiàn)得非常奇怪。事實(shí)上,它們會使得診斷代碼變得極為困難。在以前的代碼中,我們認(rèn)為如果對實(shí)例 C<JFrame>
調(diào)用 register("test")
,會發(fā)出 ClassCastException
。但并非如此;計算將繼續(xù),就仿佛數(shù)據(jù)類型轉(zhuǎn)換成功了一樣,然后在進(jìn)一步進(jìn)行計算時發(fā)出錯誤,或者更糟:用遭破壞的數(shù)據(jù)完成計算,但不向外發(fā)出任何錯誤信號。同樣,對“外露”類型參數(shù)的 instanceof
檢查將在編譯時產(chǎn)生“unchecked”警告,而且檢查將不會如期在運(yùn)行時進(jìn)行。
雙刃劍
那么,這里到底發(fā)生了什么?因?yàn)?Tiger 依靠類型擦除,所以數(shù)據(jù)類型轉(zhuǎn)換和 instanceof
測試中的外露類型參數(shù)被“擦除”為它們的上界(在前面的例子中,那將是類型 Object
)。因此,對類型參數(shù)的數(shù)據(jù)類型轉(zhuǎn)換將變成對參數(shù)上界的轉(zhuǎn)換。
同樣,instanceof
將檢查操作數(shù)是否是參數(shù)界限的 instanceof
。那根本不是我們打算做的,如果是的話,我們完全可以顯式地強(qiáng)制轉(zhuǎn)換為界限。因此,通常應(yīng)避免對類型參數(shù)使用數(shù)據(jù)類型轉(zhuǎn)換和 instanceof
檢查。
然而,有時為了編譯代碼,您必須依靠對類型參數(shù)的數(shù)據(jù)類型轉(zhuǎn)換。如果是這樣的情況,只要記住,在代碼的那一部分中,類型檢查不保險 — 要靠自己。
盡管泛型類型是制作健壯代碼的強(qiáng)大武器,但我們已經(jīng)演示了誤用它們會使代碼不再健壯而且極難診斷和修正。下次,我們將介紹 Tiger 中泛型類型的后兩個限制,并討論試圖在泛型 Java 類型系統(tǒng)中包括它們時必定會出現(xiàn)的一些問題。