java并行任务dispatch_Java模拟 双分派Double Dispatch

[最后编辑2014.9.29]

本节应用命令模式,在Java中模拟双分派。理解本节后,访问者模式(visitor pattern)手到擒来。

1. 分派

分派/ dispatch是指按照对象的实际类型为其绑定对应方法体的过程。

例如有X类及其两个子类X1、X2,它们都实现了实例方法m()——通常子类X1、X2的方法前应该加@Override,所以有3个m()。

1)现在声明X类型变量a,请问消息a.m()绑定3个中的哪一个方法呢?当前主流的面向对象语言如C++、Java、C#等,都会按照a的实际类型,为其绑定对应方法体。

【例子3-22似乎多余,删除!】

例程3-22的Test中,声明的X类型变量a、b按照配置文件分别持有X1和X2对象的引用,所以a.m()绑定它指向对象的实际类型X1的方法体,b.m()绑定X2.m()。

对于消息表达式a.m(b,c),按照一个对象的实际类型绑定对应方法体,称为单分派。当然,这个“一个对象”比较特殊,每一个消息表达式a.m(b,c)只有一个消息接收者,这个“一个对象”就是指消息接收者。所以,仅按照消息接收者的实际类型绑定实际类型提供的方法体,即单分派(singledispatch),就是面向对象中的动态绑定!

2)假设对于消息表达式a.m(b,c),如果能够按照a、b和c的实际类型为其绑定对应方法体,则称为三分派。简单起见,研究双分派(double dispatch)就够了。Java、C#等语言不支持双分派。原因你应该清楚:对于foo(X)、foo(X1)和foo(X2)这些重载的方法,Java在编译时,就为foo(b)按照b的声明类型静态绑定了foo(X)这个的方法体。

所谓的双分派,则是希望a.foo(b)能够①按照a的实际类型绑定其override的方法体,而且能够②按照b的实际类型绑定其重载的方法即foo(Y)、foo(Y1)、foo(Y2)中的适当方法体。【相关概念,可以参考《设计模式.5.11访问者模式》p223】

遗憾的是,Java不支持双分派。Java在编译时,就为foo(b)按照b的声明类型X静态绑定了foo(X)这个的方法体。(Java重载方法的匹配算法,请参考【编程导论·2.3.1】)

2. 区分重载的方法 由于动态绑定的m()方法在本讨论中属于多余,因而以Y、Y1和Y2替代X层次,而且Y、Y1和Y2都是空类体。所有需要的重载的方法foo(Y)、foo(Y1)和foo(Y2)等等,全部放在OverloadFoo类中。

例程 3 24 重载的foo

package method.command.doubleDispatch;

import static tool.Print.*;

public class OverloadFoo{

public void foo(Y y) { pln("foo(Y)"); }

public void foo(Y1 y){ pln("foo(Y1)");}

public void foo(Y2 y){ pln("foo(Y2)");}

/**

* (Run-Time Type Identification、RTTI

*/

public void foo_RTTI(Y y){

if(y instanceof Y1){

Y1 temp = (Y1)y;

foo(temp);

}else if(y instanceof Y2){

Y2 temp = (Y2)y;

foo(temp);

}else{

foo(y);

}

}

}测试代码

public static void Y单分派(){

Y y = (Y)God.create("3-18-Y");

new OverloadFoo().foo(y);

new OverloadFoo().foo_RTTI(y);

}

配置文件创建Y1对象时,输出为:

foo(Y) //Java的静态绑定

foo(Y1) //RTTI

Java中可以使用运行时类型识别(Run-Time TypeIdentification、RTTI)技术,即使用关键字instanceof判断实际类型。虽然声明类型为父类Y,程序中按照实际类型重新声明temp,并将参数向下造型。RTTI虽然代码简洁,但使用分支语句不够优雅。另外,①程序员还要注意,具体类型判断在前;②RTTI将占用较多的运行时间和空间。

[编程导论·2.3.1] 中说明:“重载一个方法,真正做的事情是定义了若干不同的方法,不过‘碰巧’使用了相同的方法名”。从调用foo(Y)的模块如客户端Test的角度看,重载的foo(Y)、foo(Y1)、foo(Y2)与不同名的fooY()、fooY1()、fooY2()没有区别。于是,有两条路线:

Test希望进行统一的调用——无视被调的方法名,我们可以采用命令模式。

当然,将重载的方法改名为fooY()、fooY1()、fooY2(),给外界感觉是双分派——这也是访问者模式(visitor pattern)采用的方式。

package method.command.doubleDispatch;

public abstract class Command{

OverloadFoo handler = new OverloadFoo();

public abstract void foo(Y y);//变化:执行者已知OverloadFoo

}

package method.command.doubleDispatch;

public class FooY1 extends Command {

@Override  public void foo(Y y)  {

handler.foo((Y1)y);

}

}//FooY和 FooY2略这个Command和

3.4 命令模式(5.2)中简单的Command接口有些小小的变化:命令的执行者已知为OverloadFoo(因为它包括了3个重载的foo方法);抽象方法foo带有参数。

Command的子类FooY1,指明执行者调用重载的foo(Y1)方法。

public static void 模拟双分派(){

Y y = (Y)God.create("3-18-Y");//Y1对象

Command cmd = new FooY();

cmd.foo(y);

cmd = new FooY1();

cmd.foo(y);

cmd = new FooY2();//任务不可执行

//cmd.foo(y);

}现在,创建一个Y对象(实际类型Y1)后,按照不同的命令,测试结果:

Y1.m()

foo(Y)

Y1.m()

foo(Y1)

命令模式,使得用户类Test无视被调的方法名,下达统一的命令foo(y);而执行者按照命令对象的不同,执行不同的方法体——这里就将重载的方法区分开来了。

0818b9ca8b590ca3270a3433284dd417.png

图1 应用命令模式

3.合并类层次

上图中有两个类层次Y和Command,图形显得比较复杂。我们发现Command的普适命令foo(Y y)在其子类FooY1中的代码为:

@Override  public void foo(Y y)  {

handler.foo((Y1)y);

}

我们如何利用Java的多态性避免这种指定性的强制类型转换呢?要点就是命令的执行者不再是固定的OverloadFoo ,而是FooY1自己——命令执行者将是Y1和Y2!

现在,开启Z系列。

命令接口Foo定义handleFoo()方法。

package method.command.doubleDispatch;

public interface Foo{

public void handleFoo();

}与X和Y对应的Z,与X不同之处为

foo(Foo  )!

Z的类层次成为Command/Foo的子类型。Z implements Foo使得Z的子类自动成为Foo的子类型,(其实Z本身不需要成为 Foo的子类型,你可以将Z的所有子类Z1、Z2 implements Foo)

package method.command.doubleDispatch;

import static tool.Print.*;

public abstract class Z implements Foo{

public void m(){

pln(" Z.m()");

}

public void foo(Foo z ){//示例代码,可以为空方法体

p(" Z.foo(Foo)-");

this.m();

}

//@Override public void handleFoo(){} //可有可无

}现在,Z1的代码如下:

package method.command.doubleDispatch;

import static tool.Print.*;

public class Z1 extends Z {

@Override public void m(){

pln(" Z1.m()");

}

/*事实上,意味着重载foo(Z1)*/

@Override public void foo(Foo z ){

p("Z1.");

z.handleFoo();//执行者z动态绑定

this.m();

}

private void foo(){

p("foo(Z1)-");

}

@Override public void handleFoo(){

this.foo();

}

}

package method.command.doubleDispatch;

import tool.God;

public class Test{

public static void Z双分派(){

Z z1 = (Z)God.create("3-18-Z1");//Z1对象

Z z2 = (Z)God.create("3-18-Z2");//Z1对象

z1.foo(z1);

z1.foo(z2);

z2.foo(z1);

z2.foo(z2);

}

}测试结果:

Z1.foo(Z1)- Z1.m()

Z1.foo(Z2)- Z1.m()

Z2.foo(Z1)- Z2.m()

Z2.foo(Z2)- Z2.m()

对于消息a.foo(b),假设a、b声明为Z类型变量,目前我们模拟了双分派Double Dispatch。

0818b9ca8b590ca3270a3433284dd417.png

图2 简洁的双分派结构

但是,这种模拟存在一个问题:Z1类中的foo(Foo z )替代了原始的重载方法foo(Z1),那么Z1类中并没有提供原始的重载方法foo(Z2)的代码。

对于客户Test而言,它声明X的对象x和Z的对象z,调用x .foo(z),应该能够找到4个方法体。你可以修改上述代码,使得消息a.foo(b),其中a声明为X类型变量、b声明为Z类型变量。

可以将重载的方法改名,使外界感觉是双分派。注意:程序中避免使用RTTI技术。

按照模拟的双分派模型,会成为2*2的表示方式,这是两个步骤的叠加而形成4个处理流程。没有双分派机制,那么区分重载操作是不可行的,对于X1和X2的对象,需要方法fooZ1(Z1)和fooZ2(Z2),与a.foo(b)对应的,有4个方法体。

网上看见一个段子:

【双重分派:

对了,你在上面的例子中体会到双重分派的实现了没有?

首先在客户程序中将具体访问者模式作为参数传递给具体元素角色(加亮的地方所示)。这便完成了一次分派。

进入具体元素角色后,具体元素角色调用作为参数的具体访问者模式中的visitor方法,同时将自己(this)作为参数传递进去。具体访问者模式再根据参数的不同来选择方法来执行(加亮的地方所示)。这便完成了第二次分派。】

你咋不说打乒乓球是n次分派?

0818b9ca8b590ca3270a3433284dd417.png

事实上,打乒乓球是我们获得命令模式的基础,【从0开始,研究一下Controller如何才能够忘记/无视被调的方法名?】

0818b9ca8b590ca3270a3433284dd417.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值