1. 分派的概念
变量被声明时的类型叫做变量的静态类型(Static Type) 又叫明显类型(Apparent Type)。变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。
根据对象的类型而对方法进行的选择,就是分派(Dispatch)。根据分派发生的时期,可以将分派分为两种,即分派分静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。方法重载(Overload)就是静态分派。(所谓的:编译时多态)
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。面向对象的语言利用动态分派来实现方法置换产生的多态性。(所谓的:运行时多态)
方法重载(静态分派)
Java通过方法重载来支持静态分派。下面考察下墨子骑马的故事。
public class Mozi
{
public void ride(Horse h)
{
System.out.println("Riding a horse");
}
public void ride(WhiteHorse wh)
{
System.out.println("Riding a white horse");
}
public void ride(BlackHorse bh)
{
System.out.println("Riding a black horse");
}
public static void main(String[] args)
{
Horse wh = new WhiteHorse();
Horse bh = new BlackHorse();
Mozi mozi = new Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
/**
* @directed
*/
private Horse lnkHorse;
}
打印了两次的“Riding a horse”。 墨子发现他骑的都是马。
两次对ride()方法的调用传入的是不同的参量,也就是wh和bh。它们虽然具有不同的真实类型,但是它们的静态类型都是一样的,均是Horse类型。
重载方法的分派是根据静态类型进行的。这个分派过程在编译时期就完成了。
动态分派
Java通过方法的置换(Overriding)支持动态分派。
String s1 = "ab";
Object o = s1 +"c";
String s = "abc";
boolean b = o.equals(s);
上述代码返回了true(基础多态就不多说了)。
2. 分派的类型
一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。
根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。单元分派语言根据一个宗量的类型(真实类型)进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。
C++和Java以及Smaltalk都是单分派语言;多分派语言的例子包括CLOS和Cecil。按照这样的区分,C++和Java就是动态的单分派语言,因为这两种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这两种语言对重载方法的分派会考虑到方法的接收者的类型和方法所有参量的类型。
在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪一个操作:一是请求的名字,二是接收者的真实类型。单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。在JAVA语言里面,如果一个操作是作用于某个类型不明的对象上面的。那么这个对象的真实类型测试仅会发生一次。这个就是动态的单分派的特征。
一言以蔽之,JAVA语言支持静态的多分派和动态的单分派。
3. 双重分派
一个方法根据两个宗量的类型来决定执行不同的代码,这就是“双分派”或者“多重分派”。Java不支持动态的多分派。但可以通过使用设计模式,在Java语言里面实现动态的双重分派(ps:就是“伪双重分派”是由两次的单分派组成)。
方案一:类型判断
在方法里使用instanceof判断真实类型,比如(java.awt.Component的源码):
protected void processEvent(AWTEvent e) {
if (e instanceof FocusEvent) {
processFocusEvent((FocusEvent)e);
} else if (e instanceof MouseEvent) {
switch(e.getID()) {
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_ENTERED:
case MouseEvent.MOUSE_EXITED:
processMouseEvent((MouseEvent)e);
break;
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
processMouseMotionEvent((MouseEvent)e);
break;
case MouseEvent.MOUSE_WHEEL:
processMouseWheelEvent((MouseWheelEvent)e);
break;
}
} else if (e instanceof KeyEvent) {
processKeyEvent((KeyEvent)e);
} else if (e instanceof ComponentEvent) {
processComponentEvent((ComponentEvent)e);
} else if (e instanceof InputMethodEvent) {
processInputMethodEvent((InputMethodEvent)e);
} else if (e instanceof HierarchyEvent) {
switch (e.getID()) {
case HierarchyEvent.HIERARCHY_CHANGED:
processHierarchyEvent((HierarchyEvent)e);
break;
case HierarchyEvent.ANCESTOR_MOVED:
case HierarchyEvent.ANCESTOR_RESIZED:
processHierarchyBoundsEvent((HierarchyEvent)e);
break;
}
}
}
这种方法实现的双重分派都格外的冗长、复杂和容易出错,也不符合“开闭原则”。
方案二:反转球
通过两次的调用来实现,比如下面剪刀石头布的游戏:
public enum Outcome { WIN, LOSE, DRAW } ///:~
interface Item {
Outcome compete(Item it);
Outcome eval(Paper p);
Outcome eval(Scissors s);
Outcome eval(Rock r);
}
class Paper implements Item {
public Outcome compete(Item it) {
return it.eval(this);
}
public Outcome eval(Paper p) {
return DRAW;
}
public Outcome eval(Scissors s) {
return WIN;
}
public Outcome eval(Rock r) {
return LOSE;
}
public String toString() {
return "Paper";
}
}
class Scissors implements Item {
public Outcome compete(Item it) {
return it.eval(this);
}
public Outcome eval(Paper p) {
return LOSE;
}
public Outcome eval(Scissors s) {
return DRAW;
}
public Outcome eval(Rock r) {
return WIN;
}
public String toString() {
return "Scissors";
}
}
class Rock implements Item {
public Outcome compete(Item it) {
return it.eval(this);
}
public Outcome eval(Paper p) {
return WIN;
}
public Outcome eval(Scissors s) {
return LOSE;
}
public Outcome eval(Rock r) {
return DRAW;
}
public String toString() {
return "Rock";
}
}
public class RoShamBo1 {
static final int SIZE = 20;
private static Random rand = new Random(47);
public static Item newItem() {
switch (rand.nextInt(3)) {
default:
case 0:
return new Scissors();
case 1:
return new Paper();
case 2:
return new Rock();
}
}
public static void match(Item a, Item b) {
System.out.println(a + " vs. " + b + ": " + a.compete(b));
}
public static void main(String[] args) {
for (int i = 0; i < SIZE; i++)
match(newItem(), newItem());
}
}
RoshamBol.match()有2个item参数,通关过Item.compete()方法开始2路分发,要判定a的类型,分发机制会在a的实际类型的compete()内部起到分发作用。compete()方法通关过eval()来为另一个类型实现第二次分发, 将自身(this)作为参数调用eval(),能够调用重载过的eval()方法,这能够保留第一次分发的类型信息,第二次分发完成时,就能知道两个Item对象的具体类型了。
这种实现也就是“访问者模式”的精华。
这种的详细解释请看:
整理至《java与模式》