至此,在這個討論 JSR-14 和 Tiger 中泛型類型的微型系列中,我們已經探討了:
泛型類型及被設計成支持它們的即將發布的功能
基本類型、受約束的泛型以及多態方法上的限制
幾個強加給這些 Java 擴展的限制
這些擴展語言的編譯器所用的實現策略如何使這些限制成為必需
在泛型類型中添加對“裸”類型參數的 new 操作的支持所帶來的影響
本月,我們將探討在可以處理 mixin(可能被期望是泛型類型中最強大的功能)之前需要先解決的問題,以此來結束對 Java 語言中泛型類型的討論。
mixin vs 包裝
mixin 是由其父類參數化的類。例如,請考慮以下這個泛型類,它繼承了它本身的類型參數:
class Scrollable<T> extends T {...}
類 Scrollable 的目的是要向 GUI 窗口小部件嵌入添加可滾動性所必需的功能性。這個泛型類的每個應用都會繼承一個不同的父類。例如, Scrollable<JTextPane> 是 JTextPane 的子類,而 Scrollable<JEditorPane> 是 JEditorPane 的子類。對比這種嵌入功能的方法和 Java Swing 庫中現有的功能性,在這個庫中,如果我們想使 JComponent 是可滾動的,必須將其“包裝”在 JScrollPane 中。
包裝不僅需要添加訪問被包裝類的功能的轉發方法,而且它還阻止我們在需要被包裝對象的實例的上下文中使用由此產生的可滾動對象(例如,我們不能將 JScrollPane 傳遞到需要 JTextPane 的實例的方法中)。通過 Scrollable 的父類將其參數化,在繼承多個超類時,我們就能保持對涉及滾動的功能的單點控制。這樣能夠使用 mixin 讓我們重新獲得多重繼承性的某些強大功能,而又沒有附帶異常。
在上面的示例中,我們甚至可以對類型參數施加約束以阻止它用於不適當的上下文中。例如,我們可能想使該類型參數強制為 JComponent 的子類:
class Scrollable<T extends JComponent> extends T {...}
那麼我們的 mixin 只能繼承 GUI 組件。
mixin 和泛型類:完美組合
通常,mixin 作為獨立語言功能部件添加到某種語言中,就象 Jam 中的那樣。但是合並 mixin 以作為泛型類型系統的一部分很吸引人,幾乎可以說魅力無窮。原因是:mixin 和泛型類都能被認為是將現有類映射到新類的 函數。
泛型類可被視為將它們的參數映射成新實例化的函數。mixin 可被視為將現有類映射成新子類的函數。通過使用泛型類型合並 mixin,我們能解決其它 mixin 公式的許多關鍵限制。
在 Java 語言的 Jam 擴展中,mixin 的超類類型沒有名稱;我們就不能在 mixin 主體中引用它。這一限制會迅速引起一連串各種其它問題。例如,在 Jam 中,禁止程序員將 this 作為參數傳遞給方法;無法對這樣的調用進行類型檢查。這一限制的影響極大,因為許多最常見的設計模式都要依賴於能夠將 this 作為參數傳遞。
請考慮訪問者模式,其中用 for 方法為復合層次結構中的每個類都定義了訪問者類。通常被訪問的類包含 accept 方法,它采用訪問者並傳遞 this 來調用該訪問者的方法。因此,在 Jam 中,訪問者模式不能和 mixin 一起使用。
將 mixin 明確表述為泛型類,我們就始終有父類的句柄,它是該類繼承的類型參數。例如,我們可以將 Scrollable 的父類引用為類型 T 。其結果是,在允許將 this 作為類型參數傳遞時沒有任何根本性的困難。
但是,將 mixin 明確表述為泛型類型時有其它一些明顯的困難。為了讓您初步體會可能產生的某些困難,我們將討論幾個突出的困難以及一些可能的解決方案。
mixin 與類型消除
在討論任何其它問題之前,我們應該先指出,與上月討論的泛型類型的功能擴展一樣,通過使用由 JSR-14 和 Tiger 使用的簡單 類型消除(type erasure)策略,不能將對 mixin 的支持添加到 Java 語言中。
要了解其原因,請考慮在繼承類型參數的類被消除時會出現什麼情況。該類會最終繼承類型參數的 界限!例如,上一個示例中類 Scrollable 的每個實例化最終都繼承類 JComponent 。那顯然不是我們所希望的。
為了通過泛型類型支持 mixin,我們 需要獲得泛型類型實例化的運行時表示。幸運的是,編碼這一信息的方法有許多,它們實際上都向後與 Tiger 兼容。
可用的超類構造函數
在我們希望允許類繼承類型參數時立即出現的緊迫問題是要決定我們能調用什麼樣的超級構造函數?請回憶:每個 Java 類構造函數都必須調用超類的構造函數。通常,通過查找超類並確保存在匹配的超級構造函數,類型檢查器確保這些超級構造函數調用會成功。
但是在我們對超類所知的一切只限於它是類型參數的實例化時,對於什麼樣的構造函數可用於給定的實例化,我們沒有任何概念。而且請注意,類型檢查器甚至不能檢查是否每個 mixin 實例化都會產生有效的超級構造函數調用。其原因是:在某些其它上下文中,mixin 的參數可能用類型參數界限實例化了。
例如,泛型類 JSplitPane<T> 可以創建 Scrollable<T> 的實例。除非我們知道將類型參數 T 實例化為 JSplitPanes 的一切方法,否則我們不能知道在 Scrollable<T> 中調用的超級構造函數是否有效。但是因為 Java 編碼允許單獨的類編譯,所以在類型檢查期間,我們不能知道 JSplitPane 的所有實例。
解決這一問題的各種方案與我們上月 第 3 部分中討論的針對檢查 new 表達式的類型參數所提出的解決方案完全一致,因為超級構造函數調用和 new 表達式都引用了給定類的同一個類構造函數。讓我們回顧一下這些解決方案:
需要一個不帶參數的(zeroary)構造函數,用於所有類型參數的實例化。
當沒有匹配的構造函數時,拋出運行時異常。
包含額外的類型參數注釋,告知我們這些實例化必須包含哪些構造函數。
就如 new 表達式的情況,前兩個解決方案有嚴重缺陷。通常在類定義中包含不帶參數的構造函數沒有任何意義。而且,當不存在任何匹配的構造函數時就拋出異常也不太理想。畢竟靜態類型檢查主要是嚴格防止那種異常。
第三種解決方案可能有點繁瑣,但是它有許多優點。注釋類型參數,其中包括所有實例化都必須擁有的構造函數集。這些注釋確切地告知我們針對類型參數,我們可以可靠地調用什麼樣的構造函數。因此,當類型參數 T 用作泛型類的超類時, T 的注釋確切地告知我們可以調用哪些超級構造函數。如果 T 不包含注釋,那麼類型檢查器會禁止它用作超類。
意外的方法覆蓋
任何 mixin 公式都會產生一個非常嚴重的問題:特定 mixin 的方法名可能與其超類的潛在實例化的方法名沖突。例如,假設類 Scrollable 包含不帶任何參數的方法 getSize 並返回一個 Size 對象,編碼了其水平和垂直尺寸。現在,我們假設類 MyTextPane ( JComponent 的子類)也包含不帶任何參數的方法 getSize ,但返回一個 int ,表示調用它的對象的屏幕面積。
產生的類顯示如下:
清單 1. 意外方法覆蓋的示例
class Scrollable<T extends JComponent> extends T {
...
Size getSize() {...}
}
class MyTextPane extends JComponent {
...
int getSize() {...}
}
new Scrollable<MyTextPane>()
隨後 mixin 實例化 Scrollable<MyTextPane> 會包含兩個帶有同樣(空)參數類型的方法 getSize ,但返回類型不一致!因為我們不能指望類 Scrollable 的程序員或 MyTextPane 的程序員預見這個有問題的 getSize 覆蓋(畢竟,他們甚至不可能在同一個開發團隊),因此我們稱之為 意外覆蓋。
當 mixin 被明確表述為泛型類時,意外覆蓋的問題特別討厭。因為 mixin 的父類可能用類型參數被實例化,因此類型檢查器就不能確定意外方法覆蓋的所有情況。而且,在意外覆蓋出現時拋出運行時異常是無法接受的,因為客戶機程序員無法預測何時將拋出這樣的異常。如果我們想編寫可靠的程序,那麼我們必須禁止在運行時出現無法預料的錯誤。
另一個解決方案是只隱藏這些相互沖突的方法中的一個,並解析所有匹配的方法調用以引用未隱藏的方法。這個解決方案的問題是我們希望諸如 Scrollable<MyTextPane> 這樣的 mixin 實例化可用於調用 Scrollable 對象的上下文以及調用 MyTextPane 對象的上下文中。隱藏 getSize 方法中的任一個都會在這兩個上下文中禁止使用 Scrollable<MyTextPane> 。
在 1998 年召開的有關編程語言原理的 ACM SIGPLAN-SIGACT 研討會(請參閱 參考資料)上,Felleisen、Flatt 和 Krishnamurthi 提出了在 mixin 不屬於泛型類型的上下文中針對該問題的一個好的解決方案:基於使用 mixin 實例化的上下文來解決對相互沖突的方法的引用。在這個解決方案中,mixin 包含有這樣的觀點:確定在名稱不一致的情況中要調用哪個方法。
在 mixin 作為泛型類型的情況中,我們可以應用同樣的解決方案。我們只要設計一些 觀點,這些觀點在泛型類型的上下文中有效,並且還允許向後兼容 JVM。在 Rice JavaPLT 實驗室中,我們已經在“A First-Class Approach to Genericity”(請參閱 參考資料)一文中提出了這樣一種解決方案。
有得必有失
正如示例、問題和可能的解決方案所演示的,在 Java 編程中繼承泛型類型以包含對 mixin 的支持會產生一種功能強大的語言,但同時也引入了一些有待克服的問題。這是典型的編程語言設計:只能通過使許多現有功能變復雜才能添加所希望的功能。在編程語言領域中,沒有任何免費的午餐。