# 16.6 多重分發
上述設計模式肯定是令人滿意的。系統內新類型的加入涉及添加或修改不同的類,但沒有必要在系統內對代碼作大范圍的改動。除此以外,RTTI并不象它在`RecycleA.java`里那樣被不當地使用。然而,我們仍然有可能更深入一步,以最“純”的角度來看待RTTI,
考慮如何在垃圾分類系統中將它完全消滅。
為達到這個目標,首先必須認識到:對所有與不同類型有特殊關聯的活動來說——比如偵測一種垃圾的具體類型,并把它置入適當的垃圾筒里——這些活動都應當通過多態性以及動態綁定加以控制。
以前的例子都是先按類型排序,再對屬于某種特殊類型的一系列元素進行操作。現在一旦需要操作特定的類型,就請先停下來想一想。事實上,多態性(動態綁定的方法調用)整個的宗旨就是幫我們管理與不同類型有特殊關聯的信息。既然如此,為什么還要自己去檢查類型呢?
答案在于大家或許不以為然的一個道理:Java只執行單一分發。也就是說,假如對多個類型未知的對象執行某項操作,Java只會為那些類型中的一種調用動態綁定機制。這當然不能解決問題,所以最后不得不人工判斷某些類型,才能有效地產生自己的動態綁定行為。
為解決這個缺陷,我們需要用到“多重分發”機制,這意味著需要建立一個配置,使單一方法調用能產生多個動態方法調用,從而在一次處理過程中正確判斷出多種類型。為達到這個要求,需要對多個類型結構進行操作:每一次分發都需要一個類型結構。下面的例子將對兩個結構進行操作:現有的Trash系列以及由垃圾筒(Trash Bin)的類型構成的一個系列——不同的垃圾或廢品將置入這些筒內。第二個分級結構并非絕對顯然的。在這種情況下,我們需要人為地創建它,以執行多重分發(由于本例只涉及兩次分發,所以稱為“雙重分發”)。
## 16.6.1 實現雙重分發
記住多態性只能通過方法調用才能表現出來,所以假如想使雙重分發正確進行,必須執行兩個方法調用:在每種結構中都用一個來判斷其中的類型。在`Trash`結構中,將使用一個新的方法調用`addToBin()`,它采用的參數是由`TypeBin`構成的一個數組。那個方法將在數組中遍歷,嘗試將自己加入適當的垃圾筒,這里正是雙重分發發生的地方。

新建立的分級結構是`TypeBin`,其中包含了它自己的一個方法,名為`add()`,而且也應用了多態性。但要注意一個新特點:`add()`已進行了“重載”處理,可接受不同的垃圾類型作為參數。因此,雙重滿足機制的一個關鍵點是它也要涉及到重載。
程序的重新設計也帶來了一個問題:現在的基類`Trash`必須包含一個`addToBin()`方法。為解決這個問題,一個最直接的辦法是復制所有代碼,并修改基類。然而,假如沒有對源碼的控制權,那么還有另一個辦法可以考慮:將`addToBin()`方法置入一個接口內部,保持`Trash`不變,并繼承新的、特殊的類型`Aluminum`,`Paper`,`Glass`以及`Cardboard`。我們在這里準備采取后一個辦法。
這個設計模式中用到的大多數類都必須設為`public`(公用)屬性,所以它們放置于自己的類內。下面列出接口代碼:
```
//: TypedBinMember.java
// An interface for adding the double dispatching
// method to the trash hierarchy without
// modifying the original hierarchy.
package c16.doubledispatch;
interface TypedBinMember {
// The new method:
boolean addToBin(TypedBin[] tb);
} ///:~
```
在`Aluminum`,`Paper`,`Glass`以及`Cardboard`每個特定的子類型內,都會實現接口`TypeBinMember`的`addToBin()`方法,但每種情況下使用的代碼“似乎”都是完全一樣的:
```
//: DDAluminum.java
// Aluminum for double dispatching
package c16.doubledispatch;
import c16.trash.*;
public class DDAluminum extends Aluminum
implements TypedBinMember {
public DDAluminum(double wt) { super(wt); }
public boolean addToBin(TypedBin[] tb) {
for(int i = 0; i < tb.length; i++)
if(tb[i].add(this))
return true;
return false;
}
} ///:~
//: DDPaper.java
// Paper for double dispatching
package c16.doubledispatch;
import c16.trash.*;
public class DDPaper extends Paper
implements TypedBinMember {
public DDPaper(double wt) { super(wt); }
public boolean addToBin(TypedBin[] tb) {
for(int i = 0; i < tb.length; i++)
if(tb[i].add(this))
return true;
return false;
}
} ///:~
//: DDGlass.java
// Glass for double dispatching
package c16.doubledispatch;
import c16.trash.*;
public class DDGlass extends Glass
implements TypedBinMember {
public DDGlass(double wt) { super(wt); }
public boolean addToBin(TypedBin[] tb) {
for(int i = 0; i < tb.length; i++)
if(tb[i].add(this))
return true;
return false;
}
} ///:~
//: DDCardboard.java
// Cardboard for double dispatching
package c16.doubledispatch;
import c16.trash.*;
public class DDCardboard extends Cardboard
implements TypedBinMember {
public DDCardboard(double wt) { super(wt); }
public boolean addToBin(TypedBin[] tb) {
for(int i = 0; i < tb.length; i++)
if(tb[i].add(this))
return true;
return false;
}
} ///:~
```
每個`addToBin()`內的代碼會為數組中的每個`TypeBin`對象調用`add()`。但請注意參數:`this`。對`Trash`的每個子類來說,`this`的類型都是不同的,所以不能認為代碼“完全”一樣——盡管以后在Java里加入參數化類型機制后便可認為一樣。這是雙重分發的第一個部分,因為一旦進入這個方法內部,便可知道到底是`Aluminum`,`Paper`,還是其他什么垃圾類型。在對`add()`的調用過程中,這種信息是通過`this`的類型傳遞的。編譯器會分析出對`add()`正確的重載版本的調用。但由于`tb[i]`會產生指向基類型`TypeBin`的一個引用,所以最終會調用一個不同的方法——具體什么方法取決于當前選擇的`TypeBin`的類型。那就是第二次分發。
下面是`TypeBin`的基類:
```
//: TypedBin.java
// Vector that knows how to grab the right type
package c16.doubledispatch;
import c16.trash.*;
import java.util.*;
public abstract class TypedBin {
Vector v = new Vector();
protected boolean addIt(Trash t) {
v.addElement(t);
return true;
}
public Enumeration elements() {
return v.elements();
}
public boolean add(DDAluminum a) {
return false;
}
public boolean add(DDPaper a) {
return false;
}
public boolean add(DDGlass a) {
return false;
}
public boolean add(DDCardboard a) {
return false;
}
} ///:~
```
可以看到,重載的`add()`方法全都會返回`false`。如果未在派生類里對方法進行重載,它就會一直返回`false`,而且調用者(目前是`addToBin()`)會認為當前`Trash`對象尚未成功加入一個集合,所以會繼續查找正確的集合。
在`TypeBin`的每一個子類中,都只有一個重載的方法會被重載——具體取決于準備創建的是什么垃圾筒類型。舉個例子來說,`CardboardBin`會重載`add(DDCardboard)`。重載的方法會將垃圾對象加入它的集合,并返回`true`。而`CardboardBin`中剩余的所有`add()`方法都會繼續返回`false`,因為它們尚未重載。事實上,假如在這里采用了參數化類型機制,Java代碼的自動創建就要方便得多(使用C++的“模板”,我們不必費事地為子類編碼,或者將`addToBin()`方法置入`Trash`里;Java在這方面尚有待改進)。
由于對這個例子來說,垃圾的類型已經定制并置入一個不同的目錄,所以需要用一個不同的垃圾數據文件令其運轉起來。下面是一個示范性的`DDTrash.dat`:
```
c16.DoubleDispatch.DDGlass:54
c16.DoubleDispatch.DDPaper:22
c16.DoubleDispatch.DDPaper:11
c16.DoubleDispatch.DDGlass:17
c16.DoubleDispatch.DDAluminum:89
c16.DoubleDispatch.DDPaper:88
c16.DoubleDispatch.DDAluminum:76
c16.DoubleDispatch.DDCardboard:96
c16.DoubleDispatch.DDAluminum:25
c16.DoubleDispatch.DDAluminum:34
c16.DoubleDispatch.DDGlass:11
c16.DoubleDispatch.DDGlass:68
c16.DoubleDispatch.DDGlass:43
c16.DoubleDispatch.DDAluminum:27
c16.DoubleDispatch.DDCardboard:44
c16.DoubleDispatch.DDAluminum:18
c16.DoubleDispatch.DDPaper:91
c16.DoubleDispatch.DDGlass:63
c16.DoubleDispatch.DDGlass:50
c16.DoubleDispatch.DDGlass:80
c16.DoubleDispatch.DDAluminum:81
c16.DoubleDispatch.DDCardboard:12
c16.DoubleDispatch.DDGlass:12
c16.DoubleDispatch.DDGlass:54
c16.DoubleDispatch.DDAluminum:36
c16.DoubleDispatch.DDAluminum:93
c16.DoubleDispatch.DDGlass:93
c16.DoubleDispatch.DDPaper:80
c16.DoubleDispatch.DDGlass:36
c16.DoubleDispatch.DDGlass:12
c16.DoubleDispatch.DDGlass:60
c16.DoubleDispatch.DDPaper:66
c16.DoubleDispatch.DDAluminum:36
c16.DoubleDispatch.DDCardboard:22
```
下面列出程序剩余的部分:
```
//: DoubleDispatch.java
// Using multiple dispatching to handle more
// than one unknown type during a method call.
package c16.doubledispatch;
import c16.trash.*;
import java.util.*;
class AluminumBin extends TypedBin {
public boolean add(DDAluminum a) {
return addIt(a);
}
}
class PaperBin extends TypedBin {
public boolean add(DDPaper a) {
return addIt(a);
}
}
class GlassBin extends TypedBin {
public boolean add(DDGlass a) {
return addIt(a);
}
}
class CardboardBin extends TypedBin {
public boolean add(DDCardboard a) {
return addIt(a);
}
}
class TrashBinSet {
private TypedBin[] binSet = {
new AluminumBin(),
new PaperBin(),
new GlassBin(),
new CardboardBin()
};
public void sortIntoBins(Vector bin) {
Enumeration e = bin.elements();
while(e.hasMoreElements()) {
TypedBinMember t =
(TypedBinMember)e.nextElement();
if(!t.addToBin(binSet))
System.err.println("Couldn't add " + t);
}
}
public TypedBin[] binSet() { return binSet; }
}
public class DoubleDispatch {
public static void main(String[] args) {
Vector bin = new Vector();
TrashBinSet bins = new TrashBinSet();
// ParseTrash still works, without changes:
ParseTrash.fillBin("DDTrash.dat", bin);
// Sort from the master bin into the
// individually-typed bins:
bins.sortIntoBins(bin);
TypedBin[] tb = bins.binSet();
// Perform sumValue for each bin...
for(int i = 0; i < tb.length; i++)
Trash.sumValue(tb[i].v);
// ... and for the master bin
Trash.sumValue(bin);
}
} ///:~
```
其中,`TrashBinSet`封裝了各種不同類型的`TypeBin`,同時還有`sortIntoBins()`方法。所有雙重分發事件都會在那個方法里發生。可以看到,一旦設置好結構,再歸類成各種`TypeBin`的工作就變得十分簡單了。除此以外,兩個動態方法調用的效率可能也比其他排序方法高一些。
注意這個系統的方便性主要體現在`main()`中,同時還要注意到任何特定的類型信息在`main()`中都是完全獨立的。只與`Trash`基類接口通信的其他所有方法都不會受到`Trash`類中發生的改變的干擾。
添加新類型需要作出的改動是完全孤立的:我們隨同`addToBin()`方法繼承`Trash`的新類型,然后繼承一個新的`TypeBin`(這實際只是一個副本,可以簡單地編輯),最后將一種新類型加入`TrashBinSet`的集合初化化過程。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物