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 平台之中。