一、引言
這篇文章,完全是為了更好的講解訪問者(Visitor)模式而寫的。讓我們進入這撲朔迷離的分派世界吧(是不是有點夸張了,汗)。
二、名詞解釋
先來解釋下分派的意思吧。。
在OO(object-oriented)語言中使用了繼承來描述不同的類之間的“社會關系”——類型層次。而這些類實例化的對象們則是對這個類型層次的體現。因此大部分OO語言的對象都存在兩個身份證:靜態類型和實際類型。所謂靜態類型,就是對象被聲明時的類型;而實際類型則便是創建對象時的類型。舉個例子:
B是A的子類。則
A object1 = new B ( );
中object1的靜態類型便是A,而實際類型卻是B。在Java語言中,編譯器會根據對象的靜態類型來檢查錯誤;而在運行時,則使用對象的真實身份。
OO還有一個重要的特點:一個類中可以存在兩個相同名稱的方法,而它們是根據參數類型的不同來區分的。
正因以上兩個原因,便產生了分派——根據類型的不同來選擇不同的方法的過程——OO語言的重要特性。
三、分類
分派可以發生在編譯期或者是運行期。因此按此標準,分派分為靜態分派和動態分派。
在程序的編譯期,只有對象的靜態類型是有效的,因此靜態分派就是根據對象(包括參數對象)的靜態類型來選擇方法的。最典型的便是方法重載(overloading)。
在運行期,動態分派會根據對象的實際類型來選擇方法。典型的例子便是方法重置(overriding)
而OO語言正是由以上兩種分派方式來提供多態特性的。
按照選擇方法時所參照的類型的個數,分派分為單分派和多分派。OO語言也因此分為了單分派(Uni-dispatch)語言和多分派(Multi-dispatch)語言。比如Smalltalk就是單分派語言,而CLOS和Cecil就是多分派語言。
說道多分派,就不得提到另一個概念:多重分派(multiple dispatch)。它指由多個單分派組成的分派過程(而多分派則往往不能分割的)。因此單分派語言可以通過多重分派的方式來實現和多分派語言一樣的效果。
那么我們熟悉的Java語言屬于哪一種分派呢?
四、Java分派實踐
先來看看在Java中最常見的特性:重載(overloading)與重置(overriding)。
下面是重載的一個具體的小例子,這是一個再簡單不過的代碼了:
//Test For OverLoading
public class Test{
public void doSomething(int i){
System.out.println("doString int = "+ i );
}
public void doSomething(String s){
System.out.println("doString String = "+ s);
}
public void doSomething(int i , String s){
System.out.println("doString int = "+ i +" String = "+ s);
}
public static void main(String[] rags){
Test t = new Test();
int i = 0;
t.doSomething(i);
}
}
沒什么好稀奇的,你對這部分知識已經熟練掌握了,那么你對下面這段代碼的用意也一定了如指掌了吧。
//Test For Overriding
public class Test{
public static void main(String[] rags){
Father f = new Father();
Father s = new Son();
f.dost();
s.dost();
}
}
class Father {
public void dost(){
System.out.println("Welcome Father!");
}
}
class Son extends Father{
public void dost(){
System.out.println("Welcome Son!");
}
}
那么下面這個代碼呢?
public class Test{
public static void main(String[] rags){
Father f = new Father();
Father s = new Son();
f.dost(1);
s.dost(4);
s.dost("dispatchTest");
//s.dost("test" , 5);
}
}
class Father {
public void dost(int i){
System.out.println("Welcome Father! int = "+ i);
}
public void dost(String s){
System.out.println("Welcome Father! String = "+ s);
}
}
class Son extends Father{
public void dost(int i){
System.out.println("Welcome Son! int = "+i);
}
public void dost(String s ,int i ){
System.out.println("Welcome Son! String = "+s+" int = "+i);
}
}
在編譯期間,編譯器根據f、s的靜態類型來為他們選擇了方法,當然都選擇了父類Father的方法。而到了運行期,則又根據s的實際類型動態的替換了原來選擇的父類中的方法。這便是結果產生的原因。
如果把上面代碼中的注釋去掉,則會出現編譯錯誤。原因便是在編譯期,編譯器根據s的靜態類型Father找不到帶有兩個參數的方法dost。
再來一個,可要注意看了:
public class Test{
//這幾個方法,唯獨的不同便在這參數上
public void dost(Father f , Father f1){
System.out.println("ff");
}
public void dost(Father f , Son s){
System.out.println("fs");
}
public void dost(Son s , Son s2){
System.out.println("ss");
}
public void dost(Son s , Father f){
System.out.println("sf");
}
public static void main(String[] rags){
Father f = new Father();
Father s = new Son();
Test t = new Test();
t.dost(f , new Father());
t.dost(f , s);
t.dost(s, f);
}
}
class Father {}
class Son extends Father{}
執行結果沒有像預期的那樣輸出ff、fs、sf而是輸出了三個ff。為什么?原因便是在編譯期,編譯器使用s的靜態類型為其選擇方法,于是這三個調用都選擇了第一個方法;而在運行期,由于Java僅僅根據方法所屬對象的實際類型來分派方法,因此這個“錯誤”就沒有被糾正而一直錯了下去……
可以看出,Java在靜態分派時,可以根據n(n>0)個參數類型來選擇不同的方法,這按照上面的定義應該屬于多分派的范圍。而在運行期時,則只能根據方法所屬對象的實際類型來進行方法的選擇,這又屬于單分派的范圍。
因此可以說Java語言支持靜態多分派和動態單分派。
五、小插曲
你看看下面的代碼會怎么執行呢?
public class Test{
public static void main(String[] rags){
Father f = new Father();
Father s = new Son();
System.out.println("f.i " + f.i);
System.out.println("s.i " + s.i);
f.dost();
s.dost();
}
}
class Father {
int i = 0 ;
public void dost(){
System.out.println("Welcome Father!");
}
}
class Son extends Father{
int i = 9 ;
public void dost(){
System.out.println("Welcome Son!");
}
}
運行結果:
\>java Test
f.i 0
s.i 0
Welcome Father!
Welcome Son!
產生的原因是Java編譯和運行程序的機制。“數據是什么”是由編譯時決定的;而“方法是哪個”則在運行時決定。
六、雙重分派
Java不能支持動態多分派,但是可以通過代碼設計來實現動態的多重分派。這里舉一個雙重分派的實現例子。
大致的思想便是通過一個參數來傳遞JVM不能判斷的類型。通過Java的動態單分派來完成一次分派后,在方法中使用instanceof來判斷參數的類型,進而決定執行哪個相關方法。
public class Test{
public static void main(String[] rags){
Father f = new Father();
Father s = new Son();
s.dosh(f);
s.dosh(s);
f.dosh(s);
f.dosh(f);
}
}
class Father {
public void dosh(Father f){
if(f instanceof Son){
System.out.println("Here is Father's Son");
}else if(f instanceof Father){
System.out.println("Here is Father's Father");
}
}
}
class Son extends Father{
public void dosh(Father f){
if(f instanceof Son){
System.out.println("Here is Son's Son");
}else if(f instanceof Father){
System.out.println("Here is Son's Father");
}
}
}
執行結果:
Here is Son's Father
Here is Son's Son
Here is Father's Son
Here is Father's Father
呵呵,慢慢在代碼中琢磨吧。用這種方式來實現雙重分派,思路比較簡單清晰。但是對于復雜一點的程序,則代碼顯得冗長,不易讀懂。而且添加新的類型比較麻煩,不是一種好的設計方案。訪問者(Visitor)模式則較好的解決了這種模式的不足。至于訪問者模式的實現……