首页 > 编程笔记 > Java笔记

访问者模式的伪动态双分派

在访问者模式中使用的就是伪动态双分派,本节我们先介绍分派的概念,再介绍伪动态双分派。

变量被声明时的类型叫做变量的静态类型(Static Type),有些人又把静态类型叫做明显类型(Apparent Type)。而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。比如:
LIst list = null;
list = new ArrayList();
上面代码声明了一个变量 list,它的静态类型是 List,而它的实际类型是 ArrayList。根据对象类型对方法进行选择,就是分派(Dispatch)。分派又分为两种,即静态分派和动态分派。

1. 静态分派

静态分派(Static Dispatch)就是按照变量的静态类型进行分派,从而确定方法的执行版本,即所谓的编译时多态,发生在编译时期。方法重载就是静态分派最典型的应用。

例 1

来看以下代码。
package net.biancheng.c.visitor;

public class OverTest {
    public void test(String string) {
        System.out.println("string");
    }

    public void test(Integer integer) {
        System.out.println("integer");
    }

    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        OverTest overTest = new OverTest();
        overTest.test(integer);
        overTest.test(string);
    }

}
静态分派判断时,根据多个判断依据(即参数类型和个数)判断出方法的版本,这就是多分派的概念。因为我们有一个以上的考量标准,所以 Java 是静态多分派的语言。

2. 动态分派

动态分派与静态分派相反,它发生在运行时期,运行时根据参数的类型,选择合适的重载方法,即所谓的运行时多态。多态就是动态分派最典型的应用。

例 2

下面举个例子,来看一下代码。
package net.biancheng.c.visitor;

interface Animal {
    void show();
}

class Dog implements Animal {
    @Override
    public void show() {
        System.out.println("小狗");
    }
}

class Cat implements Animal {

    @Override
    public void show() {
        System.out.println("小猫");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        dog.show();
        cat.show();
    }
}
运行结果为:

小狗
小猫

这里的 show() 方法无法根据 Dog 和 Cat 的静态类型判断,因为它们的静态类型都是 Animal 接口。显然,产生这样的输出结果,是因为 show() 方法的版本是在运行时判断的,这就是动态分派。

动态分派判断的方法是在运行时获取 Dog 和 Cat 的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念,这考量标准只有一个,即变量的实际引用类型。相应地,这说明 Java 是动态单分派语言。

通过前面的分析,我们知道 Java 是静态多分派、动态单分派的语言。Java 底层不支持动态双分派,但是通过使用设计模式,也可以在 Java 里实现伪动态双分派。

3. 伪动态双分派

在访问者模式中使用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是使用两个动态单分派来达到这个效果。

在《访问者模式详解》一节中 ObjectStructure 类中的 accept() 方法代码如下。
public void accept(Visitor visitor) {
    Iterator<Element> i=list.iterator();
    while(i.hasNext()) {
        ((Element) i.next()).accept(visitor);
    }     
}
这里根据 Visitor 和 Element 两个元素的实际类型来决定 accept() 方法的执行版本。

accept() 方法的调用过程分析如下:
  1. 当调用 accept() 方法时,首先根据 Element 的实际类型决定是调用 ConcreteElementA 还是 ConcreteElementB 的 accept() 方法。
  2. 这时 accept() 方法的版本已经确定,假设是 ConcreteElementA ,则它的 accept() 方法调用以下代码。
public void accept(Visitor visitor) {
    visitor.visit(this);
}
此时的 this 是 ConcreteElementA 类型,因此对应的是 Visitor 接口的 visit(ConcreteElementA element) 方法,此时需要再根据访问者的实际类型确定 visit() 方法的版本(ConcreteElementA 或 ConcreteElementB),如此一来,就完成了动态双分派的过程。

以上过程通过两次动态分派,第一次对 accept() 方法进行动态分派,第二次对访问者的 visit() 方法进行动态分派,从而达到根据两个实际类型确定一个方法的行为效果。

而原本的做法通常是传入一个接口,直接使用该接口的方法,此为动态单分派,就像策略模式一样。而在这里,accept() 方法传入的访问者接口并不是直接调用自己的 visit() 方法,而是通过 Element 的实际类型先动态分派一次,然后在分派后确定的方法版本里进行自己的动态分派。

注意:这里确定 accept(Visitor visitor) 方法是由静态分派决定的,所以这个并不在此次动态双分派的范畴内,而且静态分派是在编译器完成的,所以 accept(Visitor visitor) 方法的静态分派与访问者模式的动态双分派并没有任何关系。动态双分派说到底还是动态分派,是在运行时发生的,它与静态分派有着本质上的区别,不可以说一次动态分派加一次静态分派就是动态双分派,而且访问者模式的双分派本身也另有所指。

this 的类型不是动态分派确定的,把它写在哪个类中,静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,请小伙伴们也要区分开来。 

所有教程

优秀文章