应用案例小结——双分派:
上面的应用提及了双分派,所谓双分派是指不管类怎么变化,我们都能找到期望的方法运行。双分派意味着得到执行的操作取决于请求的种类和两个接收者的类型。
在上一节的案例中,假设我们要添加一个 Wait 的状态类,考察 Man 类和 Woman 类的反应,由于使用了双分派,只需要增加一个 Action 子类即可在客户端调用即可,不需要改动任何其他类的代码。
分派模式
多分派 (multi-dispatch):(multi-dispatch(多分派)是Visitor模式的关键,实际上Visitor模式就是提供了一种multi-dispatch(多分派) 中的double dispatch(双分派)的实现方式。
double dispatch(双分派)是multi-dispatch(多分派)的特例,由于Visitor模式涉及的是double dispatch(双分派),因此这里仅仅讨论double dispatch(双分派)的内容。实际上double dispatch(双分派)是一种很经典的技术,但是当前 的主流的面向对象程序设计语言(例如C++/Java/C#等)都并不支持多分派,仅仅支持单分派(single dispatch)。
单分派(single dispatch)的含义比较好理解,单分派(single dispatch)就是说我们在选择一个方法的时候仅仅需要根据消息接收者(receiver)的运行时型别(Run time type)。实际上这也就是我们经常提到的多态的概念(当然C++中的函数重载也是Sigle dispatch的一种实现方式)。举一个简单的例子,我们有一个基类A,A有一个虚方法f(可被子类override),D1和D2是A的两个子类,在D1和D2中我们覆写(override)了方法f。这样我们对消息f的调用,需要根据接收者A或者A的子类D1/D2的具体型别才可以确定具体是调用A的还是D1/D2的f方法。
double dispatch(双分派)则在选择一个方法的时候,不仅仅要根据消息接收者(receiver) 的运行时型别(Run time type),还要根据参数的运行时型别(Run time type)。
当然如果所有参数都考虑的话就是multi-dispatch(多分派)。也举一个简单的例子,
同于上面单分派中例子,A的虚方法 f 带了一个C型别的参数,C也是一个基类,C有也有两个具体子类E1和E2。
这样,当我们在调用消息f的时候,我们不但要根据接收者的具体型别(A、D1、D2),还要根据参数的具体型别(C、E1、E2),才可以最后确定调用的具体是哪一个方法f。
Java 实现双分派
一、动态绑定
动态绑定指程序执行期间(而不是在编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法 。
示例代码:
1 package com.dp.zhb;
2
3 public class DynamicBound {
4 public static void main(String[] args) {
5 Person person = new Man() ;
6 person.say() ;
7 }
8 }
9
10 class Person{
11 public void say(){} ;
12 }
13
14 class Man extends Person{
15 public void say(){
16 System.out.println("Hey Man");
17 }
18 }
运行结果:
调用的是 Person 对象中的 say 方法,但是实际执行的是 Man 中的方法,这就是动态绑定。
二、静态绑定
静态绑定就是指在编译期就已经确定执行哪一个方法。方法的重载(方法名相同而参数不同)就是静态绑定的,重载时,执行哪一个方法在编译期就已经确定下来。
示例代码:
1 package com.dp.zhb;
2
3 public class StaticBound {
4 public static void main(String[] args) {
5 OutputName out = new OutputName() ;
6 Person p = new Person() ;
7 Person man = new Man() ;
8 Person woman = new Woman() ;
9 out.print(p) ;
10 out.print(man) ;
11 out.print(woman) ;
12 }
13 }
14
15
16 class Person{
17 }
18
19 class Man extends Person{
20
21 }
22 class Woman extends Person{
23
24 }
25
26 class OutputName{
27 void print(Person p){
28 System.out.println("person");
29 }
30 void print(Man m){
31 System.out.println("man");
32 }
33 void print(Woman w){
34 System.out.println("woman");
35 }
36 }
运行结果:
不管在运行的时候传入的实际类型是什么,它永远都只会执行 void print(Person p)这个方法,即 : 重载是静态绑定的。
如果希望使用重载的时候,程序能够根据传入参数的实际类型动态地调用相应的方法,只能通过instanceof操作符进行类型的判断,然后再进行调用。虽然可以解决问题,但是如果子类数目很多,那么就要写很过个if else来判断类型,显然不是这种解决方案不是很合适。上述代码如下:
1 package com.dp.zhb;
2
3 public class StaticBound {
4 public static void main(String[] args) {
5 OutputName out = new OutputName() ;
6 Person p = new Person() ;
7 Person man = new Man() ;
8 Person woman = new Woman() ;
9 out.print(p) ;
10 out.print(man) ;
11 out.print(woman) ;
12 }
13 }
14
15
16 class Person{
17 }
18
19 class Man extends Person{
20
21 }
22 class Woman extends Person{
23
24 }
25
26 class OutputName{
27 void print(Person p){
28 if(p instanceof Man) print((Man)p);
29 else if (p instanceof Woman) print((Woman)p);
30 else System.out.println("person");
31 }
32 void print(Man m){
33 System.out.println("man");
34 }
35 void print(Woman w){
36 System.out.println("woman");
37 }
38 }
结果:
三、双分派
分派( dispatch)是指运行环境按照对象的实际类型为其绑定对应方法体的过程。
double dispatch(双分派)在选择一个方法的时候,不仅仅要根据消息接收者(receiver) 的运行时型别(Run time type),还要根据参数的运行时型别(Run time type)。这里的消息接收者其实就是方法的调用者。具体来讲就是,对于消息表达式a.m(b),双分派能够按照a和b的实际类型为其绑定对应方法体。
来看一个双分派的例子:
1 package com.dp.zhb;
2
3 class Father {
4 public void accept(Execute exe){
5 exe.method(this);
6 }
7 }
8 class Son1 extends Father{
9 @Override
10 public void accept(Execute exe){
11 exe.method(this);
12 }
13 }
14 class Son2 extends Father{
15 @Override
16 public void accept(Execute exe){
17 exe.method(this);
18 }
19 }
20
21 class Execute {
22 public void method(Father father){
23 System.out.println("This is Father's method");
24 }
25
26 public void method(Son1 son){
27 System.out.println("This is Son1's method");
28 }
29
30 public void method(Son2 son){
31 System.out.println("This is Son2's method");
32 }
33 }
34
35 public class Test {
36 public static void main(String[] args){
37 Father father = new Father();
38 Father s1 = new Son1();
39 Father s2 = new Son2();
40 Execute exe = new Execute();
41 father.accept(exe);
42 s1.accept(exe);
43 s2.accept(exe);
44 }
45 }
运行结果:
通俗的解释一下,就是重载是静态绑定,重写是动态绑定,双分派把重写放在重载之前,以实现在运行时动态判断执行那个子类的方法。上面的例子中,首先依据重写(Java的多态)找到对应的accept方法,然后accept方法中调用method方法, 并把当前类的this传入method,传入this这步就属于是静态绑定,在编译器就确定好的。比如在Father类中:
1 class Father {
2 public void accept(Execute exe){
3 exe.method(this);
4 }
5 }
method(this)就对应了Execute类中的
1 public void method(Father father){
2 System.out.println("This is Father's method");
3 }
双分派问题
说到访问者模式就不得不提一下双分派的问题,什么是双分派呢,我们先来解释一下什么是单分派和多分派。
根据对象的类型而对方法进行的选择,就是分派(Dispatch)
一、分派的类型
一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。
根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。
单元分派语言根据一个宗量的类型(真实类型)进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。
C++和Java以及Smaltalk都是单分派语言;多分派语言的例子包括CLOS和Cecil。按照这样的区分,C++和Java就是动态的单分派语言,因为这两种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这两种语言对重载方法的分派会考虑到方法的接收者的类型和方法所有参量的类型。
二、根据分派发生的时期,可以将分派分为两种,即分派分
静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。方法重载(Overload)就是静态分派。(所谓的:编译时多态)
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。面向对象的语言利用动态分派来实现方法置换产生的多态性。(所谓的:运行时多态)
单分派语言处理一个操作是根据请求者的名称和收到的参数决定的,在Java中有静态绑定和动态绑定直说,它的实现是依据重载(overload)和覆写(override)实现的
示例:
举个简单的例子,演员可以演电影角色,一个演员可以扮演多个角色
角色:
1 public interface Role {
2 //演员要扮演的角色
3 }
4
5 -------------------------
6 public class KunFuRole implements Role {
7 //扮演一个武功高强的角色
8 }
演员:
1 public abstract class AbsActor {
2 //演员能扮演角色
3 public void act(Role role){
4 System.out.println("演员能扮演任何角色");
5 }
6 //只演功夫戏的角色
7 public void act(KunFuRole role){
8 System.out.println("这个演员只演功夫戏");
9 }
10 }
11 ----------------------------------------------
12 public class YoungActor extends AbsActor {
13 @Override
14 public void act(KunFuRole role){
15 System.err.println("年轻演员喜欢演功夫戏");
16 }
17 }
18 ---------------------------------------------
19 public class OldActor extends AbsActor {
20 //不演功夫角色
21 @Override
22 public void act(KunFuRole role) {
23 System.err.println("太老了,演不动了");
24 }
25 }
此时覆写和重载都实现了。
测试类:
1 public class Main {
2 public static void main(String[] args) {
3 //定义一个演员
4 AbsActor absActor = new OldActor();
5 //定义一个角色
6 Role role = new KunFuRole();
7 //开始演戏
8 absActor.act(role);
9 absActor.act(new KunFuRole());
10 }
11 }
结果:
重载在编译器时期就决定了要调用哪个方法,它是根据role的表面类型而决定调用act(Role role)方法,这是静态绑定;而Actor的执行方法act则是由其实际类型决定的。这是动态绑定。
一个演员可以扮演多个角色,系统要适应这种变化,也就是根据演员和角色两个对象类型完成不同的操作任务,此时引入访问者模式就可以了
1 public interface Role {
2 //演员要扮演的角色
3 public void accpet(AbsActor absActor);
4 }
5 -----------------------------------------------
6 public class KunFuRole implements Role {
7 //扮演一个武功高强的角色
8 @Override
9 public void accpet(AbsActor absActor) {
10 absActor.act(this);
11 }
12
13 }
14 ---------------------------------------------
15 public class IdiotRole implements Role{
16 //扮演一个弱智角色
17 @Override
18 public void accpet(AbsActor absActor) {
19 absActor.act(this);
20 }
21 }
测试类
1 public class Main {
2 public static void main(String[] args) {
3 //定义一个老演员
4 AbsActor oldActor = new OldActor();
5 //定义一个年轻演员
6 AbsActor youngActor = new YoungActor();
7 //定义一个角色
8 Role role = new KunFuRole();
9
10 role.accpet(oldActor);
11 role.accpet(youngActor);
12 }
13 }
运行结果:
这样无论演员类如何变化,我们都能找到期待的方法运行,这就是双分派
双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型,它是双分派的一个特例,这里也可以看出java是一个支持双分派的单分派语言。
ps:Java不支持动态的多分派。但可以通过使用设计模式,在Java语言里面实现动态的双重分派(ps:就是“伪双重分派”是由两次的单分派组成)
JAVA语言支持静态的多分派和动态的单分派