這一系列主要討論在 Java 編程中添加泛型類型,本文是其中的一篇,將研究還未討論過(guò)的有關(guān)使用泛型的兩個(gè)限制之一,即添加對(duì)裸類型參數(shù)的 new
操作的支持(如類 C<T>
中的 new T()
)。
正如我上個(gè)月所提到的那樣,Tiger 和 JSR-14 通過(guò)使用“類型消除(type erasure)”對(duì) Java 語(yǔ)言實(shí)現(xiàn)泛型類型。使用類型消除(type erasure),泛型類型僅用于類型檢查;然后,用它們的上界替換它們。由此定義可知:消除將與如 new T()
之類的表達(dá)式?jīng)_突。
如果假定 T
的界限是 Object
,那么這一表達(dá)式將被消除為 new Object()
,并且不管對(duì) T
如何實(shí)例化(String
、List
、URLClassLoader
等等),new
操作將產(chǎn)生一個(gè)新的 Object
實(shí)例。顯然,這不是我們想要的。
要添加對(duì)表達(dá)式(如 new T()
)的支持,以及添加對(duì)我們上次討論過(guò)的其它與類型相關(guān)的操作(如數(shù)據(jù)類型轉(zhuǎn)換和 instanceof
表達(dá)式)的支持,我們必須采用某種實(shí)現(xiàn)策略而不是類型消除(如對(duì)于每個(gè)泛型實(shí)例化,使用獨(dú)立的類)。但對(duì)于 new
操作,需要處理其它問(wèn)題。
尤其是,為了實(shí)現(xiàn)對(duì) Java 語(yǔ)言添加這種支持,必須對(duì)許多基本的語(yǔ)言設(shè)計(jì)問(wèn)題作出決定。
首先,為了對(duì)類型參數(shù)構(gòu)造合法的 new
表達(dá)式(如 new T()
),必須確保我們調(diào)用的構(gòu)造函數(shù)對(duì)于 T
的每個(gè)實(shí)例化都有效。但由于我們只知道 T
是其已聲明界限的子類型,所以我們不知道 T
的某一實(shí)例化將有什么構(gòu)造函數(shù)。要解決這一問(wèn)題,可以用下述三種方法之一:
- 要求類型參數(shù)的所有實(shí)例化都包括不帶參數(shù)的(zeroary)構(gòu)造函數(shù)。
- 只要泛型類的運(yùn)行時(shí)實(shí)例化沒(méi)有包括所需的構(gòu)造函數(shù),就拋出異常。
- 修改語(yǔ)言的語(yǔ)法以包括更詳盡的類型參數(shù)界限。
第 1 種方法:需要不帶參數(shù)的構(gòu)造函數(shù)
只要求類型參數(shù)的所有實(shí)例化都包括不帶參數(shù)的構(gòu)造函數(shù)。該解決方案的優(yōu)點(diǎn)是非常簡(jiǎn)單。使用這種方法也有先例。
處理類似問(wèn)題的現(xiàn)有 Java 技術(shù)(象 JavaBean 技術(shù))就是通過(guò)要求一個(gè)不帶參數(shù)的構(gòu)造函數(shù)來(lái)解決問(wèn)題的。然而,該方法的一個(gè)主要缺點(diǎn)是:對(duì)于許多類,沒(méi)有合理的不帶參數(shù)的構(gòu)造函數(shù)。
例如,表示非空容器的任何類在構(gòu)造函數(shù)中必然使用表示其元素的參數(shù)。包括不帶參數(shù)的構(gòu)造函數(shù)將迫使我們先創(chuàng)建實(shí)例,然后再進(jìn)行本來(lái)可以在構(gòu)造函數(shù)調(diào)用中完成的初始化。但該實(shí)踐會(huì)導(dǎo)致問(wèn)題的產(chǎn)生(您可能想要閱讀 2002 年 4 月發(fā)表的本專欄文章“The Run-on Initializer bug pattern”,以獲取詳細(xì)信息;請(qǐng)參閱參考資料。)
第 2 種方法:當(dāng)缺少所需構(gòu)造函數(shù)時(shí),拋出異常
處理該問(wèn)題的另一種方法是:只要泛型類的運(yùn)行時(shí)實(shí)例化沒(méi)有包括所需構(gòu)造函數(shù),就拋出異常。請(qǐng)注意:必須在運(yùn)行時(shí)拋出異常。因?yàn)?Java 語(yǔ)言的遞增式編譯模型,所以我們無(wú)法靜態(tài)地確定所有將在運(yùn)行時(shí)發(fā)生的泛型類的實(shí)例化。例如,假設(shè)我們有如下一組泛型類:
|
現(xiàn)在,在類 D<S>
中,構(gòu)造了類 C<S>
的實(shí)例。然后,在類 C
的主體中,將調(diào)用 S
的不帶參數(shù)的構(gòu)造函數(shù)。這種不帶參數(shù)的構(gòu)造函數(shù)存在嗎?答案當(dāng)然取決于 S
的實(shí)例化!
比方說(shuō),如果 S
被實(shí)例化為 String
,那么答案是“存在”。如果它被實(shí)例化為 Integer
,那么答案是“不存在”。但是,當(dāng)編譯類 D
和 C
時(shí),我們不知道其它類會(huì)構(gòu)造什么樣的 D<S>
實(shí)例化。即使我們有可用于分析的整個(gè)程序(我們幾乎從來(lái)沒(méi)有這樣的 Java 程序),我們還是必須進(jìn)行代價(jià)相當(dāng)高的流分析來(lái)確定潛在的構(gòu)造函數(shù)問(wèn)題可能會(huì)出現(xiàn)在哪里。
此外,這一技術(shù)所產(chǎn)生的錯(cuò)誤種類對(duì)于程序員來(lái)說(shuō)很難診斷和修復(fù)。例如,假設(shè)程序員只熟悉類 D
的頭。他知道 D
的類型參數(shù)的界限是缺省界限(Object
)。如果得到那樣的信息,他沒(méi)有理由相信滿足聲明類型界限(如 D<Integer>
)的 D
的實(shí)例化將會(huì)導(dǎo)致錯(cuò)誤。事實(shí)上,它在相當(dāng)長(zhǎng)的時(shí)間里都不會(huì)引起錯(cuò)誤,直到最后有人調(diào)用方法 makeC
以及(最終)對(duì) C
的實(shí)例化調(diào)用方法 makeT
。然后,我們將得到一個(gè)報(bào)告的錯(cuò)誤,但這將在實(shí)際問(wèn)題發(fā)生很久以后 — 類 D
的糟糕實(shí)例化。
還有,對(duì)所報(bào)告錯(cuò)誤的堆棧跟蹤甚至可能不包括任何對(duì)這個(gè)糟糕的 D
實(shí)例的方法調(diào)用!現(xiàn)在,讓我們假設(shè)程序員無(wú)權(quán)訪問(wèn)類 C
的源代碼。他對(duì)問(wèn)題是什么或如何修正代碼將毫無(wú)頭緒,除非他設(shè)法聯(lián)系類 C
的維護(hù)者并獲得線索。
第 3 種方法:修改語(yǔ)法以獲得更詳盡的界限
另一種可能性是修改語(yǔ)言語(yǔ)法以包括更詳盡的類型參數(shù)界限。這些界限可以指定一組可用的構(gòu)造函數(shù),它們必須出現(xiàn)在參數(shù)的每一個(gè)實(shí)例化中。因而,在泛型類定義內(nèi)部,唯一可調(diào)用的構(gòu)造函數(shù)是那些在界限中聲明的構(gòu)造函數(shù)。
同樣,實(shí)例化泛型類的客戶機(jī)類必須使用滿足對(duì)構(gòu)造函數(shù)存在所聲明的約束的類來(lái)這樣做。參數(shù)聲明將充當(dāng)類與其客戶機(jī)之間的契約,這樣我們可以靜態(tài)地檢查這兩者是否遵守契約。
與另外兩種方法相比,該方法有許多優(yōu)點(diǎn),它允許我們保持第二種方法的可表達(dá)性以及與第一種方法中相同的靜態(tài)檢查程度。但它也有需要克服的問(wèn)題。
首先,類型參數(shù)聲明很容易變得冗長(zhǎng)。我們或許需要某種形式的語(yǔ)法上的甜頭,使這些擴(kuò)充的參數(shù)聲明還過(guò)得去。另外,如果在 Tiger 以后的版本中添加擴(kuò)充的參數(shù)聲明,那么我們必須確保這些擴(kuò)充的聲明將與現(xiàn)有的已編譯泛型類兼容。
如果將對(duì)泛型類型的與類型相關(guān)的操作的支持添加到 Java 編程中,那么它采用何種形式還不清楚。但是,從哪種方法將使 Java 代碼盡可能地保持健壯(以及使在它遭到破壞時(shí)盡可能容易地修正)的觀點(diǎn)看,第三個(gè)選項(xiàng)無(wú)疑是最適合的。
然而,new
表達(dá)式有另一個(gè)更嚴(yán)重的問(wèn)題。
更嚴(yán)重的問(wèn)題是類定義中可能存在多態(tài)遞歸。當(dāng)泛型類在其自己的主體中實(shí)例化其本身時(shí),發(fā)生多態(tài)遞歸。例如,考慮下面的錯(cuò)誤示例:
清單 2. 自引用的泛型類 |
假設(shè)客戶機(jī)類創(chuàng)建新的 C<Object>
實(shí)例,并調(diào)用(比方說(shuō))nest(1000)
。然后,在執(zhí)行方法 nest()
的過(guò)程中,將構(gòu)造新的實(shí)例化 C<C<Object>>
,并且對(duì)它調(diào)用 nest(999)
。然后,將構(gòu)造實(shí)例化 C<C<C<Object>>>
,以此類推,直到構(gòu)造 1000 個(gè)獨(dú)立的類 C
的實(shí)例化。當(dāng)然,我隨便選擇數(shù)字 1000;通常,我們無(wú)法知道在運(yùn)行時(shí)哪些整數(shù)將被傳遞到方法 nest
。事實(shí)上,可以將它們作為用戶輸入傳入。
為什么這成為問(wèn)題呢?因?yàn)槿绻覀兺ㄟ^(guò)為每個(gè)實(shí)例化構(gòu)造獨(dú)立類來(lái)支持泛型類型的與類型相關(guān)的操作,那么,在程序運(yùn)行以前,我們無(wú)法知道我們需要構(gòu)造哪些類。但是,如果類裝入器為它所裝入的每個(gè)類查找現(xiàn)有類文件,那么它會(huì)如何工作呢?
同樣,這里有幾種可能的解決辦法:
- 對(duì)程序可以產(chǎn)生的泛型類的實(shí)例化數(shù)目設(shè)置上限。
- 靜態(tài)禁止多態(tài)遞歸。
- 在程序運(yùn)行時(shí)隨需構(gòu)造新的實(shí)例化類。
第 1 種:對(duì)實(shí)例化數(shù)設(shè)置上限
我們對(duì)程序可以產(chǎn)生的泛型類的實(shí)例化數(shù)目設(shè)置上限。然后,在編譯期間,我們可以對(duì)一組合法的實(shí)例化確定有限界限,并且僅為該界限中的所有實(shí)例化生成類文件。
該方法類似于在 C++ 標(biāo)準(zhǔn)模板庫(kù)中完成的事情(這使我們有理由擔(dān)心它不是一個(gè)好方法)。該方法的問(wèn)題是,和為錯(cuò)誤的構(gòu)造函數(shù)調(diào)用報(bào)告錯(cuò)誤一樣,程序員將無(wú)法預(yù)知其程序的某一次運(yùn)行將崩潰。例如,假設(shè)實(shí)例化數(shù)的界限為 42,并且使用用戶提供的參數(shù)調(diào)用先前提到的 nest()
方法。那么,只要用戶輸入小于 42 的數(shù),一切都正常。當(dāng)用戶輸入 43 時(shí),這一計(jì)劃不周的設(shè)計(jì)就會(huì)失敗?,F(xiàn)在,設(shè)想一下可憐的代碼維護(hù)者,他所面對(duì)的任務(wù)是重新組合代碼并試圖弄清楚幻數(shù) 42 有什么特殊之處。
第 2 種:靜態(tài)禁止多態(tài)遞歸
為什么我們不向編譯器發(fā)出類似“靜態(tài)禁止多態(tài)遞歸”這樣的命令呢?(唉!要是那么簡(jiǎn)單就好了。)當(dāng)然,包括我在內(nèi)的許多程序員都會(huì)反對(duì)這種策略,它抑制了許多重要設(shè)計(jì)模式的使用。
例如,在泛型類 List<T>
中,您真的想要防止 List<List<T>>
的構(gòu)造嗎?從方法返回這種列表對(duì)于構(gòu)建許多很常用的數(shù)據(jù)結(jié)構(gòu)很有用。事實(shí)證明我們無(wú)法防止多態(tài)遞歸,即使我們想要那樣,也是如此。就象靜態(tài)檢測(cè)糟糕的泛型構(gòu)造函數(shù)調(diào)用一樣,禁止多態(tài)遞歸會(huì)與遞增式類編譯發(fā)生沖突。我們先前的簡(jiǎn)單示例(其中,多態(tài)遞歸作為一個(gè)簡(jiǎn)單直接的自引用發(fā)生)會(huì)使這一事實(shí)變得模糊。但是,自引用對(duì)于在不同時(shí)間編譯的大多數(shù)類常常采用任意的間接級(jí)別。再提一次,那是因?yàn)橐粋€(gè)泛型類可以用其自己的類型參數(shù)來(lái)實(shí)例化另一個(gè)泛型類。
下面的示例涉及兩個(gè)類之間的多態(tài)遞歸:
清單 3. 相互遞歸的多態(tài)遞歸 |
在類 C
或 D
中顯然沒(méi)有多態(tài)遞歸,但象 new D<C<Object>>().nest(1000)
之類的表達(dá)式將引起類 C
的 1000 次實(shí)例化。
或許,我們可以將新屬性添加到類文件中,以表明類中所有不同泛型類型實(shí)例化,然后在編譯其它類時(shí)分析這些實(shí)例化,以進(jìn)行遞歸。但是,我們還是必須向程序員提供奇怪的和不直觀的錯(cuò)誤消息。
在上面的代碼中,我們?cè)谀睦飯?bào)告錯(cuò)誤呢?在類 D
的編譯過(guò)程中還是在包含不相干表達(dá)式 new D<C<Object>>().nest(1000)
的客戶機(jī)類的編譯過(guò)程中呢?無(wú)論是哪一種,除非程序員有權(quán)訪問(wèn)類 C
的源代碼,否則他無(wú)法預(yù)知何時(shí)會(huì)發(fā)生編譯錯(cuò)誤。
第 3 種:實(shí)時(shí)構(gòu)造新的實(shí)例化類
另一種方法是在程序運(yùn)行時(shí)按需構(gòu)造新的實(shí)例化類。起先,這種方法似乎與 Java 運(yùn)行時(shí)完全不兼容。但實(shí)際上,實(shí)現(xiàn)該策略所需的全部就是使用一個(gè)修改的類裝入器,它根據(jù)“模板(template)”類文件構(gòu)造新的實(shí)例化類。
JVM 規(guī)范已經(jīng)允許程序員使用修改的類裝入器;事實(shí)上,許多流行的 Java 應(yīng)用程序(如 Ant、JUnit 和 DrJava)都使用它們。該方法的缺點(diǎn)是:修改的類裝入器必須與其應(yīng)用程序一起分布,以在較舊的 JVM 上運(yùn)行。因?yàn)轭愌b入器往往比較小,所以這個(gè)開銷不會(huì)大。
讓我們研究一下該方法的工作示例。
NextGen 示例:修改的類裝入器
前一種方法 — 用按需構(gòu)造泛型類型實(shí)例化的修改的類裝入器解決多態(tài)遞歸問(wèn)題 — 被 Java 語(yǔ)言的 NextGen 擴(kuò)展所采用。修改的類裝入器使用看上去幾乎與普通類文件完全一樣的模板文件,不同的是這個(gè)模板文件在常量池中有一些“洞”,在裝入時(shí)為每個(gè)實(shí)例化類填充這些“洞”。非泛型類不受影響。
在 Rice 大學(xué) JavaPLT 編程語(yǔ)言實(shí)驗(yàn)室,我們最近發(fā)布了 NextGen 編譯器的原型,它是 GJ 泛型 Java 編譯器的一種擴(kuò)展,這種擴(kuò)展支持類型參數(shù)的與類型相關(guān)的操作(數(shù)據(jù)類型轉(zhuǎn)換、instanceof
測(cè)試和 new
表達(dá)式)。在該原型實(shí)現(xiàn)中,我們使用了一個(gè)修改的類裝入器來(lái)支持多態(tài)遞歸。可以免費(fèi)下載該原型(請(qǐng)參閱參考資料)。
正如上述考慮事項(xiàng)所演示的那樣,將成熟的運(yùn)行時(shí)支持添加到泛型 Java 要解決許多微妙的設(shè)計(jì)問(wèn)題。如果這些問(wèn)題處理得不當(dāng),那么可表達(dá)性和健壯性的降低會(huì)輕易地抵消泛型類型的好處。但愿 Java 編程會(huì)繼續(xù)朝著維持這些屬性的高度表達(dá)性和健壯性的方向發(fā)展。
下一次,我們將通過(guò)討論或許是功能最強(qiáng)大的應(yīng)用泛型類型的方法 — 將 mixin(具有參數(shù)父類型的類)添加到語(yǔ)言中 — 來(lái)結(jié)束對(duì)泛型類型的討論。我們會(huì)將這種 mixin 的表現(xiàn)方式與先前討論的這種功能強(qiáng)大的語(yǔ)言特性相關(guān)聯(lián),討論通過(guò)泛型類型添加 mixin 的優(yōu)缺點(diǎn)。