診斷 Java 代碼: 輕松掌握 Java 泛型Java Tiger 版本和 JSR-14 原型編譯器中的泛型指南 ![]() |
![]() |
|
級(jí)別: 初級(jí)
Eric E. Allen
, 博士研究生, Rice 大學(xué) Java 編程語(yǔ)言團(tuán)隊(duì)
2003 年 5 月 14 日
本月的 診斷 Java 代碼介紹泛型類(lèi)型(generic type)和支持它們的特性,計(jì)劃在 2003 年末發(fā)布的 Tiger,也就是 Java V1.5 中打算包含這些泛型和特性。Eric Allen 提供了代碼樣本,這些樣本通過(guò)重點(diǎn)描述諸如基本類(lèi)型的限制、受限泛型和多態(tài)方法之類(lèi)的 Tiger 特性來(lái)說(shuō)明泛型類(lèi)型的優(yōu)缺點(diǎn)(即將發(fā)表的專(zhuān)欄文章將討論其它特性,比如 Tiger 中泛型類(lèi)型的特定表現(xiàn)以及可能擴(kuò)展為 Tiger 之外的泛型類(lèi)型)。請(qǐng)通過(guò)單擊文章頂部或底部的 討論進(jìn)入 論壇,與作者和其他讀者分享您對(duì)本文的心得體會(huì)。
J2SE 1.5 - 代號(hào)為 Tiger - 計(jì)劃在 2003 年年底發(fā)布。我一直都熱衷于盡可能多地收集有關(guān)即將推出的新技術(shù)的預(yù)告信息,因此我將撰寫(xiě)一系列的文章,討論可從 V1.5 中獲得的新的和經(jīng)過(guò)重組的特性,本文是第一篇。我特別想談?wù)劮盒皖?lèi)型并重點(diǎn)講述在 Tiger 中為了支持它們而進(jìn)行的更改和調(diào)整。
在許多方面,Tiger 肯定是迄今為止在 Java 編程方面(包括對(duì)源語(yǔ)言語(yǔ)法的重大擴(kuò)展)所取得的最大進(jìn)步。Tiger 中計(jì)劃進(jìn)行的最顯著的變化是添加泛型類(lèi)型,正如在 JSR-14 原型編譯器中所預(yù)先展示的那樣(您可以立即免費(fèi)下載該編譯器;請(qǐng)參閱 參考資料)。
讓我們從介紹泛型類(lèi)型是什么以及添加了什么特性來(lái)支持它們開(kāi)始吧。
數(shù)據(jù)類(lèi)型轉(zhuǎn)換和錯(cuò)誤
為理解泛型類(lèi)型為何如此有用,我們要將注意力轉(zhuǎn)向 Java 語(yǔ)言中最容易引發(fā)錯(cuò)誤的因素之一 - 需要不斷地將表達(dá)式向下類(lèi)型轉(zhuǎn)換(downcast)為比其靜態(tài)類(lèi)型更為具體的數(shù)據(jù)類(lèi)型(請(qǐng)參閱 參考資料中的“The Double Descent bug pattern”,以了解進(jìn)行數(shù)據(jù)類(lèi)型轉(zhuǎn)換時(shí),可能會(huì)碰到的麻煩的某些方面)。
程序中的每個(gè)向下類(lèi)型轉(zhuǎn)換對(duì)于 ClassCastException
而言都是潛在的危險(xiǎn),應(yīng)當(dāng)盡量避免它們。但是在 Java 語(yǔ)言中它們通常是無(wú)法避免的,即便在設(shè)計(jì)優(yōu)良的程序中也是如此。
在 Java 語(yǔ)言中進(jìn)行向下類(lèi)型轉(zhuǎn)換最常見(jiàn)的原因在于,經(jīng)常以專(zhuān)用的方式來(lái)使用類(lèi),這限制了方法調(diào)用所返回的參數(shù)可能的運(yùn)行時(shí)類(lèi)型。例如,假定往 Hashtable
中添加元素并從中檢索元素。那么在給定的程序中,被用作鍵的元素類(lèi)型和存儲(chǔ)在散列表中的值類(lèi)型,將不能是任意對(duì)象。通常,所有的鍵都是某一特定類(lèi)型的實(shí)例。同樣地,存儲(chǔ)的值將共同具有比 Object
更具體的公共類(lèi)型。
但是在目前現(xiàn)有的 Java 語(yǔ)言版本中,不可能將散列表的特定鍵和元素聲明為比 Object
更具體的類(lèi)型。在散列表上執(zhí)行插入和檢索操作的類(lèi)型特征符告訴我們只能插入和刪除任意對(duì)象。例如, put
和 get
操作的說(shuō)明如下所示:
清單 1. 插入/檢索類(lèi)型說(shuō)明表明只能是任意對(duì)象
|
因此,當(dāng)我們從類(lèi) Hashtable
的實(shí)例檢索元素時(shí),比如,即使我們知道在 Hashtable
中只放了 String
,而類(lèi)型系統(tǒng)也只知道所檢索的值是 Object
類(lèi)型。在對(duì)檢索到的值進(jìn)行任何特定于 String
的操作之前,必須將它強(qiáng)制轉(zhuǎn)換為 String
,即使是將檢索到的元素添加到同一代碼塊中,也是如此!
清單 2. 將檢索到的值強(qiáng)制轉(zhuǎn)換成 String
|
請(qǐng)注意 main
方法主體部分的第三行中需要進(jìn)行的數(shù)據(jù)類(lèi)型轉(zhuǎn)換。因?yàn)?Java 類(lèi)型系統(tǒng)相當(dāng)薄弱,因此代碼會(huì)因象上面那樣的數(shù)據(jù)類(lèi)型轉(zhuǎn)換而漏洞百出。這些數(shù)據(jù)類(lèi)型轉(zhuǎn)換不僅使 Java 代碼變得更加拖沓冗長(zhǎng),而且它們還降低了靜態(tài)類(lèi)型檢查的價(jià)值(因?yàn)槊總€(gè)數(shù)據(jù)類(lèi)型轉(zhuǎn)換都是一個(gè)選擇忽略靜態(tài)類(lèi)型檢查的偽指令)。我們?cè)撊绾螖U(kuò)展該類(lèi)型系統(tǒng),從而不必回避它呢?
![]() ![]() |
![]()
|
要消除如上所述的數(shù)據(jù)類(lèi)型轉(zhuǎn)換,有一種普遍的方法,就是用 泛型類(lèi)型來(lái)增大 Java 類(lèi)型系統(tǒng)。可以將泛型類(lèi)型看作是類(lèi)型“函數(shù)”;它們通過(guò)類(lèi)型變量進(jìn)行參數(shù)化,這些類(lèi)型變量可以根據(jù)上下文用各種類(lèi)型參數(shù)進(jìn)行 實(shí)例化。
例如,與簡(jiǎn)單地定義類(lèi) Hashtable
不同,我們可以定義泛型類(lèi) Hashtable<Key, Value>
,其中 Key
和 Value
是類(lèi)型參數(shù)。除了類(lèi)名后跟著尖括號(hào)括起來(lái)的一系列類(lèi)型參數(shù)聲明之外,在 Tiger 中定義這樣的泛型類(lèi)的語(yǔ)法和用于定義普通類(lèi)的語(yǔ)法很相似。例如,可以按照如下所示的那樣定義自己的泛型 Hashtable
類(lèi):
清單 3. 定義泛型 Hashtable 類(lèi)
|
然后可以引用這些類(lèi)型參數(shù),就像我們?cè)陬?lèi)定義主體內(nèi)引用普通類(lèi)型那樣,如下所示:
清單 4. 像引用普通類(lèi)型那樣引用類(lèi)型參數(shù)
|
類(lèi)型參數(shù)的作用域就是相應(yīng)類(lèi)定義的主體部分(除了靜態(tài)成員之外)(在下一篇文章中,我們將討論為何 Tiger 實(shí)現(xiàn)中有這樣的“怪習(xí)”,即必須對(duì)靜態(tài)成員進(jìn)行此項(xiàng)限制。請(qǐng)留意!)。
創(chuàng)建一個(gè)新的 Hashtable
實(shí)例時(shí),必須傳遞類(lèi)型參數(shù)以指定 Key
和 Value
的類(lèi)型。傳遞類(lèi)型參數(shù)的方式取決于我們打算如何使用 Hashtable
。在上面的示例中,我們真正想要做的是創(chuàng)建 Hashtable
實(shí)例,它只將 Integer
映射為 String
。可以用新的 Hashtable
類(lèi)來(lái)完成這件事:
清單 5. 創(chuàng)建將 Integer 映射為 String 的實(shí)例
|
現(xiàn)在不再需要數(shù)據(jù)類(lèi)型轉(zhuǎn)換了。請(qǐng)注意用來(lái)實(shí)例化泛型類(lèi) Hashtable
的語(yǔ)法。就像泛型類(lèi)的類(lèi)型參數(shù)用尖括號(hào)括起來(lái)那樣,泛型類(lèi)型應(yīng)用程序的參數(shù)也是用尖括號(hào)括起來(lái)的。
清單 6. 除去不必要的數(shù)據(jù)類(lèi)型轉(zhuǎn)換
|
當(dāng)然,程序員若只是為了能使用泛型類(lèi)型而必須重新定義所有的標(biāo)準(zhǔn)實(shí)用程序類(lèi)(比如 Hashtable
和 List
)的話(huà),則可能會(huì)是一項(xiàng)浩大的工程。幸好,Tiger 為用戶(hù)提供了所有 Java 集合類(lèi)的泛型版本,因此我們不必自己動(dòng)手來(lái)重新定義它們了。此外,這些類(lèi)能與舊代碼和新的泛型代碼一起無(wú)縫工作(下個(gè)月,我們會(huì)說(shuō)明如何做到這一點(diǎn))。
![]() ![]() |
![]()
|
Tiger 中類(lèi)型變量的限制之一就是,它們必須用引用類(lèi)型進(jìn)行實(shí)例化 - 基本類(lèi)型不起作用。因此,在上面這個(gè)示例中,無(wú)法完成創(chuàng)建從 int
映射到 String
的 Hashtable
。
這很遺憾,因?yàn)檫@意味著只要您想把基本類(lèi)型用作泛型類(lèi)型的參數(shù),您就必須把它們組裝為對(duì)象。另一方面,當(dāng)前的這種情況是最糟的;您不能將 int
作為鍵傳遞給 Hashtable
,因?yàn)樗械逆I都必須是 Object
類(lèi)型。
我們真正想看到的是,基本類(lèi)型可以自動(dòng)進(jìn)行包裝(boxing)和解包裝(unboxing),類(lèi)似于用 C# 所進(jìn)行的操作(或者比后者更好)。遺憾的是,Tiger 不打算包括基本類(lèi)型的自動(dòng)包裝(但是人們可以一直期待 Java 1.6 中出現(xiàn)該功能!)。
![]() ![]() |
![]()
|
有時(shí)我們想限制可能出現(xiàn)的泛型類(lèi)的類(lèi)型實(shí)例化。在上面這個(gè)示例中,類(lèi) Hashtable
的類(lèi)型參數(shù)可以用我們想用的任何類(lèi)型參數(shù)進(jìn)行實(shí)例化,但是對(duì)于其它某些類(lèi),我們或許想將可能的類(lèi)型參數(shù)集限定為給定類(lèi)型 范圍內(nèi)的子類(lèi)型。
例如,我們可能想定義泛型 ScrollPane
類(lèi),它引用普通的帶有滾動(dòng)條功能的 Pane
。被包含的 Pane
的運(yùn)行時(shí)類(lèi)型通常會(huì)是類(lèi) Pane
的子類(lèi)型,但是靜態(tài)類(lèi)型就只是 Pane
。
有時(shí)我們想用 getter 檢索被包含的 Pane
,但是希望 getter 的返回類(lèi)型盡可能具體些。我們可能想將類(lèi)型參數(shù) MyPane
添加到 ScrollPane
中,該類(lèi)型參數(shù)可以用 Pane
的任何子類(lèi)進(jìn)行實(shí)例化。然后可以用這種形式的子句: extends Bound
來(lái)說(shuō)明 MyPane
的聲明,從而來(lái)設(shè)定 MyPane
的范圍:
清單 7. 用 extends 子句來(lái)說(shuō)明 MyPane 聲明
|
當(dāng)然,我們可以完全不使用顯式的范圍,只要能確保沒(méi)有用不適當(dāng)?shù)念?lèi)型來(lái)實(shí)例化類(lèi)型參數(shù)。
為什么要自找麻煩在類(lèi)型參數(shù)上設(shè)定范圍呢?這里有兩個(gè)原因。首先,范圍使我們?cè)黾恿遂o態(tài)類(lèi)型檢查功能。有了靜態(tài)類(lèi)型檢查,就能保證泛型類(lèi)型的每次實(shí)例化都符合所設(shè)定的范圍。
其次,因?yàn)槲覀冎李?lèi)型參數(shù)的每次實(shí)例化都是這個(gè)范圍之內(nèi)的子類(lèi),所以可以放心地調(diào)用類(lèi)型參數(shù)實(shí)例出現(xiàn)在這個(gè)范圍之內(nèi)的任何方法。如果沒(méi)有對(duì)參數(shù)設(shè)定顯式的范圍,那么缺省情況下范圍是 Object
,這意味著我們不能調(diào)用范圍實(shí)例在 Object
中未曾出現(xiàn)的任何方法。
![]() ![]() |
![]()
|
除了用類(lèi)型參數(shù)對(duì)類(lèi)進(jìn)行參數(shù)化之外,用類(lèi)型參數(shù)對(duì)方法進(jìn)行參數(shù)化往往也同樣很有用。泛型 Java 編程用語(yǔ)中,用類(lèi)型進(jìn)行參數(shù)化的方法被稱(chēng)為 多態(tài)方法(Polymorphic method)。
多態(tài)方法之所以有用,是因?yàn)橛袝r(shí)候,在一些我們想執(zhí)行的操作中,參數(shù)與返回值之間的類(lèi)型相關(guān)性原本就是泛型的,但是這個(gè)泛型性質(zhì)不依賴(lài)于任何類(lèi)級(jí)的類(lèi)型信息,而且對(duì)于各個(gè)方法調(diào)用都不相同。
例如,假定想將 factory
方法添加到 List
類(lèi)中。這個(gè)靜態(tài)方法只帶一個(gè)參數(shù),也將是 List 唯一的元素(直到添加了其它元素)。因?yàn)槲覀兿M?List
成為其所包含的元素類(lèi)型的泛型,所以希望靜態(tài) factory
方法帶有類(lèi)型變量 T
這一參數(shù)并返回 List<T>
的實(shí)例。
但是我們確實(shí)希望該類(lèi)型變量 T
能在方法級(jí)別上進(jìn)行聲明,因?yàn)樗鼤?huì)隨每次單獨(dú)的方法調(diào)用而發(fā)生改變(而且,正如我在下一篇文章中將討論的那樣,Tiger 設(shè)計(jì)的“怪習(xí)”規(guī)定靜態(tài)成員不在類(lèi)級(jí)類(lèi)型參數(shù)的范疇之內(nèi))。Tiger 讓我們通過(guò)將類(lèi)型參數(shù)作為方法聲明的前綴,從而在單獨(dú)的方法級(jí)別上聲明類(lèi)型參數(shù)。例如,可以按照如下所示的那樣為 factory
方法 make
添加前綴:
清單 8. 將類(lèi)型參數(shù)作為前綴添加到方法聲明
|
除了多態(tài)方法中所增加的靈活性之外,Tiger 中還增加了一個(gè)優(yōu)點(diǎn)。Tiger 使用類(lèi)型推斷機(jī)制,根據(jù)參數(shù)類(lèi)型來(lái)自動(dòng)推斷出多態(tài)方法的類(lèi)型。這可以大大減少方法調(diào)用的繁瑣和復(fù)雜性。例如,如果想調(diào)用 make
方法來(lái)構(gòu)造包含 new Integer(0)
的 List<Integer>
新實(shí)例,那么只需編寫(xiě):
清單 9. 強(qiáng)制 make 構(gòu)造新實(shí)例
|
然后會(huì)自動(dòng)地從方法參數(shù)中推斷出類(lèi)型參數(shù)的實(shí)例化。
![]() ![]() |
![]()
|
正如我們所見(jiàn)到的那樣,在 Java 語(yǔ)言中添加泛型類(lèi)型肯定會(huì)大大增強(qiáng)我們使用靜態(tài)類(lèi)型系統(tǒng)的能力。學(xué)習(xí)如何使用泛型類(lèi)型相當(dāng)簡(jiǎn)單,但是同樣也需要避免一些缺陷。在接下來(lái)的文章中,我們將討論如何充分使用將出現(xiàn)在 Tiger 中的泛型類(lèi)型的特定表現(xiàn),以及一些缺陷。我們還將研究對(duì)泛型 Java 類(lèi)型工具的擴(kuò)展,我們期盼這些工具可以出現(xiàn)在仍處于設(shè)計(jì)階段的 Java 平臺(tái)之中。
![]() ![]() |
![]()
|
- 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文.
- 請(qǐng)參與有關(guān)本文的 論壇(您也可以單擊文章頂部或底部的 討論來(lái)訪(fǎng)問(wèn)該論壇)。
- 通過(guò)下載 JSR-14 原型編譯器(您必須是 Java Developer Connection 的注冊(cè)成員)來(lái)進(jìn)一步學(xué)習(xí) Java 編程中的泛型。它包括了用擴(kuò)展語(yǔ)言編寫(xiě)的原型編譯器的源代碼、包含了用于運(yùn)行和自舉編譯器的類(lèi)文件的 JAR 文件,以及包含了集合類(lèi)存根的 JAR 文件。
- Eric Allen 寫(xiě)了一本有關(guān)錯(cuò)誤模式主題的新書(shū): Bug Patterns in Java(Apress,2002),該書(shū)提出了一種診斷和調(diào)試計(jì)算機(jī)程序的方法論,這種方法論側(cè)重于錯(cuò)誤模式、極端編程方法和生成功能強(qiáng)大的、可測(cè)的且可擴(kuò)展的軟件的方法。
- 請(qǐng)參閱“ Double Descent 錯(cuò)誤模式”( developerWorks,2001 年 4 月),以了解在進(jìn)行數(shù)據(jù)類(lèi)型轉(zhuǎn)換時(shí),可能會(huì)碰到的麻煩的某些方面。
- IntelliJ 的 IDEA 開(kāi)發(fā)環(huán)境是值得一試的“好點(diǎn)子”,它包括了 J2EE 高速網(wǎng)絡(luò)應(yīng)用程序開(kāi)發(fā)功能部件、一個(gè)功能強(qiáng)大的代碼檢查工具,以及一個(gè)用于第三方插件支持的開(kāi)放式 API。
- 并且別忘了嘗試一下用于 J2SE 和 J2EE 開(kāi)發(fā)的高性能代碼分析引擎 - 來(lái)自 OmniCore 的 CodeGuide。它早已通過(guò) JSR-14 原型編譯器為 Java 代碼中的泛型類(lèi)型提供了 IDE 支持。
- Martin Fowler 的 網(wǎng)站包含了許多有關(guān)有效重構(gòu)的有用信息。
- 研究“ 設(shè)計(jì)“可測(cè)試的”應(yīng)用程序”( developerWorks,2001 年 9 月),以了解牢記測(cè)試來(lái)構(gòu)建代碼設(shè)計(jì)基礎(chǔ)的七項(xiàng)原則。
- 在 診斷 Java 代碼專(zhuān)欄文章摘要 中,可以查閱 Eric Allen 專(zhuān)欄的 developerWorks 資源庫(kù) - 從錯(cuò)誤模式到可測(cè)性再到設(shè)計(jì)策略。
- 通過(guò)閱讀 Java 社區(qū)過(guò)程(Java Community Process)的建議書(shū): JSR-14來(lái)了解有關(guān)將泛型類(lèi)型添加到 Java 代碼中的討論。
- Keith Turner 在“ 編譯時(shí)使用 Generic Java 捕獲更多的錯(cuò)誤”( developerWorks,2001 年 3 月)中提出了關(guān)于本主題的另一種觀點(diǎn)。
- 來(lái)自 IBM 研究部門(mén)(IBM Research)的論文“ Automatic Code Generation from Design Patterns”(PDF)描述了使設(shè)計(jì)模式實(shí)現(xiàn)自動(dòng)化的工具的體系結(jié)構(gòu)和實(shí)現(xiàn)。
-
Diagnosing Java code系列文章中的下面這兩篇文章可以幫助您充實(shí)有關(guān)泛型類(lèi)型和 Java 類(lèi)型系統(tǒng)的知識(shí):“ “殺手組合”― mixin、Jam 和單元測(cè)試”(2002 年 12 月)和“ 擁護(hù)靜態(tài)類(lèi)型的理由”(2002 年 6 月)。
- 在 developerWorksJava 技術(shù)專(zhuān)區(qū) 上查找其它大量的 Java 技術(shù)參考資料。
![]() ![]() |
![]()
|
![]() |
||
|
![]() |
Eric 是 DrJava 項(xiàng)目(為初學(xué)者設(shè)計(jì)的開(kāi)放源碼 Java IDE)的項(xiàng)目經(jīng)理和創(chuàng)建人之一;他還是 Rice 大學(xué)用于 NextGen 編程語(yǔ)言的實(shí)驗(yàn)性編譯器的主要開(kāi)發(fā)人員,NextGen 編程語(yǔ)言是 Java 語(yǔ)言添加了一些實(shí)驗(yàn)性功能的擴(kuò)展。Eric 為在線(xiàn)雜志 JavaWorld主持幾個(gè) Java 論壇。除了這些活動(dòng)之外,Eric 還為 Rice 大學(xué)計(jì)算機(jī)科學(xué)系的本科生講授軟件工程這門(mén)課。可以通過(guò) eallen@cs.rice.edu與 Eric 聯(lián)系。 |