註:本文非原創,點擊查看原帖
JAVA的泛類型,類似c++中的模板template,從JDK1.5開始支援編寫泛類型了。
列如:
①jdk1.5以前的代碼
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);
}
}
裡面有強制的資料類型轉化。
而在java(jdk1.5)的原始碼中則沒有了資料的強制轉化
class Hashtable<Key, Value> {
...
Value put(Key k, Value v) {...}
Value get(Key k) {...}
}
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");
String s = h.get(new Integer(0)); System.out.println(s); }
}
②泛型的多態
class Utilities {
<T extends Object> public static List<T> make(T first) {
return new List<T>(first);
}
}
強制 make 構造新執行個體
Utilities.make(Integer(0))
③ 受限泛型
有時我們想限制可能出現的泛型類的類型執行個體化。在上面這個樣本中,類 Hashtable 的型別參數可以用我們想用的任何型別參數進行執行個體化,但是對於其它某些類,我們或許想將可能的型別參數集限定為給定類型 範圍內的子類型。
例如,我們可能想定義泛型 ScrollPane 類,它引用普通的帶有捲軸功能的 Pane 。被包含的 Pane 的運行時類型通常會是類 Pane 的子類型,但是靜態類型就只是 Pane 。
有時我們想用 getter 檢索被包含的 Pane ,但是希望 getter 的傳回型別儘可能具體些。我們可能想將型別參數 MyPane 添加到 ScrollPane 中,該型別參數可以用 Pane 的任何子類進行執行個體化。然後可以用這種形式的子句: extends Bound 來說明 MyPane 的聲明,從而來設定 MyPane 的範圍:
清單 7. 用 extends 子句來說明 MyPane 聲明
class ScrollPane<MyPane extends Pane> { ... }
④ 其它泛型的例子
class C<T> {
static T member;
C(T t) { member = t; }
T getMember() { return member; }
public static void main(String[] args) {
C<String> c = new C<String>("test");
System.out.println(c.getMember().toString());
new C<Integer>(new Integer(1));
System.out.println(c.getMember().toString());
}
}
import java.util.Hashtable;
interface Registry {
public void register(Object o);
}
class C<T> implements Registry {
int counter = 0;
Hashtable<Integer, T> values;
public C() {
values = new Hashtable<Integer, T>();
}
public void register(Object o) {
values.put(new Integer(counter), (T)o);
counter++;
}
}
⑤
輕鬆掌握 Java 泛型,第 3 部分
克服 JSR-14 原型編譯器中泛型的限制
層級: 初級
Eric E. Allen, 博士研究生, Java 程式設計語言團隊,Rice 大學
2003 年 6 月 09 日
Java 開發人員和研究員 Eric Allen 繼續討論 JSR-14 和 Tiger 中的泛型型別,並著眼於在泛型型別中添加 naked 型別參數的 new 操作支援這一分支。
這一系列主要討論在 Java 編程中添加泛型型別,本文是其中的一篇,將研究還未討論過的有關使用泛型的兩個限制之一,即添加對裸型別參數的 new 操作的支援(如類 C<T> 中的 new T() )。
正如我 上個月所提到的那樣,Tiger 和 JSR-14 通過使用“類型消除(type erasure)”對 Java 語言實現泛型型別。使用類型消除(type erasure),泛型型別僅用於類型檢查;然後,用它們的上界替換它們。由此定義可知:消除將與如 new T() 之類的運算式衝突。
如果假定 T 的界限是 Object ,那麼這一運算式將被消除為 new Object() ,並且不管對 T 如何執行個體化( String 、 List 、 URLClassLoader 等等), new 操作將產生一個新的 Object 執行個體。顯然,這不是我們想要的。
要添加對錶達式(如 new T() )的支援,以及添加對我們上次討論過的其它與類型相關的操作(如資料類型轉換和 instanceof 運算式)的支援,我們必須採用某種實現策略而不是類型消除(如對於每個泛型執行個體化,使用獨立的類)。但對於 new 操作,需要處理其它問題。
尤其是,為了實現對 Java 語言添加這種支援,必須對許多基本的語言設計問題作出決定。
有效建構函式調用
首先,為了對型別參數構造合法的 new 運算式(如 new T() ),必須確保我們調用的建構函式對於 T 的每個執行個體化都有效。但由於我們只知道 T 是其已聲明界限的子類型,所以我們不知道 T 的某一執行個體化將有什麼建構函式。要解決這一問題,可以用下述三種方法之一:
要求型別參數的所有執行個體化都包括不帶參數的(zeroary)建構函式。
只要泛型類的運行時執行個體化沒有包括所需的建構函式,就拋出異常。
修改語言的文法以包括更詳盡的型別參數界限。
第 1 種方法:需要不帶參數的建構函式
只要求型別參數的所有執行個體化都包括不帶參數的建構函式。該解決方案的優點是非常簡單。使用這種方法也有先例。
處理類似問題的現有 Java 技術(象 JavaBean 技術)就是通過要求一個不帶參數的建構函式來解決問題的。然而,該方法的一個主要缺點是:對於許多類,沒有合理的不帶參數的建構函式。
例如,表示非空容器的任何類在建構函式中必然使用表示其元素的參數。包括不帶參數的建構函式將迫使我們先建立執行個體,然後再進行本來可以在建構函式調用中完成的初始化。但該實踐會導致問題的產生(您可能想要閱讀 2002 年 4 月發表的本專欄文章“The Run-on Initializer bug pattern”,以擷取詳細資料;請參閱 參考資料。)
第 2 種方法:當缺少所需建構函式時,拋出異常
處理該問題的另一種方法是:只要泛型類的運行時執行個體化沒有包括所需建構函式,就拋出異常。請注意:必須在運行時拋出異常。因為 Java 語言的遞增式編譯模型,所以我們無法靜態地確定所有將在運行時發生的泛型類的執行個體化。例如,假設我們有如下一組泛型類:
清單 1.“裸”型別參數的 New 操作
class C<T> {
T makeT() {
return new T();
}
}
class D<S> {
C<S> makeC() {
return new C<S>();
}
}
現在,在類 D<S> 中,構造了類 C<S> 的執行個體。然後,在類 C 的主體中,將調用 S 的不帶參數的建構函式。這種不帶參數的建構函式存在嗎?答案當然取決於 S 的執行個體化!
比方說,如果 S 被執行個體化為 String ,那麼答案是“存在”。如果它被執行個體化為 Integer ,那麼答案是“不存在”。但是,當編譯類 D 和 C 時,我們不知道其它類會構造什麼樣的 D<S> 執行個體化。即使我們有可用於分析的整個程式(我們幾乎從來沒有這樣的 Java 程式),我們還是必須進行代價相當高的串流分析來確定潛在的建構函式問題可能會出現在哪裡。
此外,這一技術所產生的錯誤種類對於程式員來說很難診斷和修複。例如,假設程式員只熟悉類 D 的頭。他知道 D 的型別參數的界限是預設界限( Object )。如果得到那樣的資訊,他沒有理由相信滿足宣告類型界限(如 D<Integer> )的 D 的執行個體化將會導致錯誤。事實上,它在相當長的時間裡都不會引起錯誤,直到最後有人調用方法 makeC 以及(最終)對 C 的執行個體化調用方法 makeT 。然後,我們將得到一個報告的錯誤,但這將在實際問題發生很久以後 ― 類 D 的糟糕執行個體化。
還有,對所報告錯誤的堆疊追蹤甚至可能不包括任何對這個糟糕的 D 執行個體的方法調用!現在,讓我們假設程式員無權訪問類 C 的原始碼。他對問題是什麼或如何修正代碼將毫無頭緒,除非他設法聯絡類 C 的維護者並獲得線索。
第 3 種方法:修改文法以獲得更詳盡的界限
另一種可能性是修改語言文法以包括更詳盡的型別參數界限。這些界限可以指定一組可用的建構函式,它們必須出現在參數的每一個執行個體化中。因而,在泛型類定義內部,唯一可調用的建構函式是那些在界限中聲明的建構函式。
同樣,執行個體化泛型類的客戶機類必須使用滿足對建構函式存在所聲明的約束的類來這樣做。參數聲明將充當類與其客戶機之間的契約,這樣我們可以靜態地檢查這兩者是否遵守契約。
與另外兩種方法相比,該方法有許多優點,它允許我們保持第二種方法的可表達性以及與第一種方法中相同的靜態檢查程度。但它也有需要克服的問題。
首先,型別參數聲明很容易變得冗長。我們或許需要某種形式的文法上的甜頭,使這些擴充的參數聲明還過得去。另外,如果在 Tiger 以後的版本中添加擴充的參數聲明,那麼我們必須確保這些擴充的聲明將與現有的已編譯泛型類相容。
如果將對泛型型別的與類型相關的操作的支援添加到 Java 編程中,那麼它採用何種形式還不清楚。但是,從哪種方法將使 Java 代碼儘可能地保持健壯(以及使在它遭到破壞時儘可能容易地修正)的觀點看,第三個選項無疑是最適合的。
然而, new 運算式有另一個更嚴重的問題。
多態遞迴
更嚴重的問題是類定義中可能存在 多態遞迴。當泛型類在其自己的主體中執行個體化其本身時,發生多態遞迴。例如,考慮下面的錯誤樣本:
清單 2. 自引用的泛型類
class C<T> {
public Object nest(int n) {
if (n == 0) return this;
else return new C<C<T>>().nest(n - 1);
}
}
假設客戶機類建立新的 C<Object> 執行個體,並調用(比方說) nest(1000) 。然後,在執行方法 nest() 的過程中,將構造新的執行個體化 C<C<Object>> ,並且對它調用 nest(999) 。然後,將構造執行個體化 C<C<C<Object>>> ,以此類推,直到構造 1000 個獨立的類 C 的執行個體化。當然,我隨便選擇數字 1000;通常,我們無法知道在運行時哪些整數將被傳遞到方法 nest 。事實上,可以將它們作為使用者輸入傳入。
為什麼這成為問題呢?因為如果我們通過為每個執行個體化構造獨立類來支援泛型型別的與類型相關的操作,那麼,在程式運行以前,我們無法知道我們需要構造哪些類。但是,如果類裝入器為它所裝入的每個類尋找現有類檔案,那麼它會如何工作呢?
同樣,這裡有幾種可能的解決辦法:
對程式可以產生的泛型類的執行個體化數目設定上限。
靜態禁止多態遞迴。
在程式運行時隨需構造新的執行個體化類。
第 1 種:對執行個體化數設定上限
我們對程式可以產生的泛型類的執行個體化數目設定上限。然後,在編譯期間,我們可以對一組合法的執行個體化確定有限界限,並且僅為該界限中的所有執行個體化產生類檔案。
該方法類似於在 C++ 標準模板庫中完成的事情(這使我們有理由擔心它不是一個好方法)。該方法的問題是,和為錯誤的建構函式調用報告錯誤一樣,程式員將無法預知其程式的某一次運行將崩潰。例如,假設執行個體化數的界限為 42,並且使用使用者提供的參數調用先前提到的 nest() 方法。那麼,只要使用者輸入小於 42 的數,一切都正常。當使用者輸入 43 時,這一計劃不周的設計就會失敗。現在,設想一下可憐的代碼維護者,他所面對的任務是重新組合代碼並試圖弄清楚幻數 42 有什麼特殊之處。
第 2 種:靜態禁止多態遞迴
為什麼我們不向編譯器發出類似“靜態禁止多態遞迴”這樣的命令呢?(唉!要是那麼簡單就好了。)當然,包括我在內的許多程式員都會反對這種策略,它抑制了許多重要設計模式的使用。
例如,在泛型類 List<T> 中,您真的想要防止 List<List<T>> 的構造嗎?從方法返回這種列表對於構建許多很常用的資料結構很有用。事實證明我們無法防止多態遞迴,即使我們想要那樣,也是如此。就象靜態檢測糟糕的泛型建構函式調用一樣,禁止多態遞迴會與遞增式類編譯發生衝突。我們先前的簡單樣本(其中,多態遞迴作為一個簡單直接的自引用發生)會使這一事實變得模糊。但是,自引用對於在不同時間編譯的大多數類常常採用任意的間接層級。再提一次,那是因為一個泛型類可以用其自己的型別參數來執行個體化另一個泛型類。
下面的樣本涉及兩個類之間的多態遞迴:
清單 3. 相互遞迴的多態遞迴
class C<T> {
public Object potentialNest(int n) {
if (n == 0) return this;
else return new D<T>().nest(n - 1);
}
}
class D<S> {
public Object nest(int n) {
return new C<C<S>>().nest(n);
}
}
在類 C 或 D 中顯然沒有多態遞迴,但象 new D<C<Object>>().nest(1000) 之類的運算式將引起類 C 的 1000 次執行個體化。
或許,我們可以將新屬性添加到類檔案中,以表明類中所有不同泛型型別執行個體化,然後在編譯其它類時分析這些執行個體化,以進行遞迴。但是,我們還是必須向程式員提供奇怪的和不直觀的錯誤訊息。
在上面的代碼中,我們在哪裡報告錯誤呢?在類 D 的編譯過程中還是在包含不相干運算式 new D<C<Object>>().nest(1000) 的客戶機類的編譯過程中呢?無論是哪一種,除非程式員有權訪問類 C 的原始碼,否則他無法預知何時會發生編譯錯誤。
第 3 種:即時構造新的執行個體化類
另一種方法是在程式運行時按需構造新的執行個體化類。起先,這種方法似乎與 Java 運行時完全不相容。但實際上,實現該策略所需的全部就是使用一個修改的類裝入器,它根據“模板(template)”類檔案構造新的執行個體化類。
JVM 規範已經允許程式員使用修改的類裝入器;事實上,許多流行的 Java 應用程式(如 Ant、JUnit 和 DrJava)都使用它們。該方法的缺點是:修改的類裝入器必須與其應用程式一起分布,以在較舊的 JVM 上運行。因為類裝入器往往比較小,所以這個開銷不會大。