本章的各種設計方案都在努力避免使用RTTI,這或許會給大家留下“RTTI有害”的印象(還記得可憐的goto嗎,由於給人印象不佳,根本就沒有放到Java裡來)。但實際情況並非絕對如此。正確地說,應該是RTTI使用不當才“有害”。我們之所以想避免RTTI的使用,是由於它的錯誤運用會造成擴展性受到損害。而我們事前提出的目標就是能向系統自由加入新類型,同時保證對周圍的代碼造成盡可能小的影響。由於RTTI常被濫用(讓它查找系統中的每一種類型),會造成代碼的擴展能力大打折扣——添加一種新類型時,必須找出使用了RTTI的所有代碼。即使僅遺漏了其中的一個,也不能從編譯器那裡得到任何幫助。
然而,RTTI本身並不會自動產生非擴展性的代碼。讓我們再來看一看前面提到的垃圾回收例子。這一次准備引入一種新工具,我把它叫作TypeMap。其中包含了一個Hashtable(散列表),其中容納了多個Vector,但接口非常簡單:可以添加(add())一個新對象,可以獲得(get())一個Vector,其中包含了屬於某種特定類型的所有對象。對於這個包含的散列表,它的關鍵在於對應的Vector裡的類型。這種設計方案的優點(根據Larry O'Brien的建議)是在遇到一種新類型的時候,TypeMap會動態加入一種新類型。所以不管什麼時候,只要將一種新類型加入系統(即使在運行期間添加),它也會正確無誤地得以接受。
我們的例子同樣建立在c16.Trash這個“包”(Package)內的Trash類型結構的基礎上(而且那兒使用的Trash.dat文件可以照搬到這裡來)。
//: DynaTrash.java // Using a Hashtable of Vectors and RTTI // to automatically sort trash into // vectors. This solution, despite the // use of RTTI, is extensible. package c16.dynatrash; import c16.trash.*; import java.util.*; // Generic TypeMap works in any situation: class TypeMap { private Hashtable t = new Hashtable(); public void add(Object o) { Class type = o.getClass(); if(t.containsKey(type)) ((Vector)t.get(type)).addElement(o); else { Vector v = new Vector(); v.addElement(o); t.put(type,v); } } public Vector get(Class type) { return (Vector)t.get(type); } public Enumeration keys() { return t.keys(); } // Returns handle to adapter class to allow // callbacks from ParseTrash.fillBin(): public Fillable filler() { // Anonymous inner class: return new Fillable() { public void addTrash(Trash t) { add(t); } }; } } public class DynaTrash { public static void main(String[] args) { TypeMap bin = new TypeMap(); ParseTrash.fillBin("Trash.dat",bin.filler()); Enumeration keys = bin.keys(); while(keys.hasMoreElements()) Trash.sumValue( bin.get((Class)keys.nextElement())); } } ///:~
盡管功能很強,但對TypeMap的定義是非常簡單的。它只是包含了一個散列表,同時add()負擔了大部分的工作。添加一個新類型時,那種類型的Class對象的句柄會被提取出來。隨後,利用這個句柄判斷容納了那類對象的一個Vector是否已存在於散列表中。如答案是肯定的,就提取出那個Vector,並將對象加入其中;反之,就將Class對象及新Vector作為一個“鍵-值”對加入。
利用keys(),可以得到對所有Class對象的一個“枚舉”(Enumeration),而且可用get(),可通過Class對象獲取對應的Vector。
filler()方法非常有趣,因為它利用了ParseTrash.fillBin()的設計——不僅能嘗試填充一個Vector,也能用它的addTrash()方法試著填充實現了Fillable(可填充)接口的任何東西。filter()需要做的全部事情就是將一個句柄返回給實現了Fillable的一個接口,然後將這個句柄作為參數傳遞給fillBin(),就象下面這樣:
ParseTrash.fillBin("Trash.dat", bin.filler());
為產生這個句柄,我們采用了一個“匿名內部類”(已在第7章講述)。由於根本不需要用一個已命名的類來實現Fillable,只需要屬於那個類的一個對象的句柄即可,所以這裡使用匿名內部類是非常恰當的。
對這個設計,要注意的一個地方是盡管沒有設計成對歸類加以控制,但在fillBin()每次進行歸類的時候,都會將一個Trash對象插入bin。
通過前面那些例子的學習,DynaTrash類的大多數部分都應當非常熟悉了。這一次,我們不再將新的Trash對象置入類型Vector的一個bin內。由於bin的類型為TypeMap,所以將垃圾(Trash)丟進垃圾筒(Bin)的時候,TypeMap的內部歸類機制會立即進行適當的分類。在TypeMap裡遍歷並對每個獨立的Vector進行操作,這是一件相當簡單的事情:
Enumeration keys = bin.keys(); while(keys.hasMoreElements()) Trash.sumValue( bin.get((Class)keys.nextElement()));
就象大家看到的那樣,新類型向系統的加入根本不會影響到這些代碼,亦不會影響TypeMap中的代碼。這顯然是解決問題最圓滿的方案。盡管它確實嚴重依賴RTTI,但請注意散列表中的每個鍵-值對都只查找一種類型。除此以外,在我們增加一種新類型的時候,不會陷入“忘記”向系統加入正確代碼的尴尬境地,因為根本就沒有什麼代碼需要添加。