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