Java的单分派与双分派以及访问者模式的关系

引言

在学习访问者模式的过程中了解到了单双分派这一词,本文将重点为大家解释Java中的单分派与双分派到底是什么,以及为什么会与访问者模式扯上关系

首先,我们给出一个定义:“Java是一种支持双分派的单分派语言”。

那么我们必须知道什么是分派,我所理解的分派对要执行的方法的确定,用通俗的话来说就是我需要知道我执行的是哪个对象的哪个方法。

那么为什么会出现单分派以及多分派?我们不是可以通过方法名来确定我们要调什么方法吗?
是的,若要调用一个方法,方法名是最关键的一个标识,这是基础,总不能我们不用知道我们要调用的是哪个方法我们就能够去调用到我们想要调用的方法吧,这不要干程序员吧,咱去当仙人多好。因此我们此时能够确定一件事,那就是调用一个方法需要基于一个确定的方法名。
但是仅仅只有方法名还不够,因为会出现同名方法呀(方法重载)。我要问张三借钱,可是你叫张三,他也叫张三,那我要还给哪个张三。因此我们还需要除去方法名以外的标识来确定一个方法。而这些标识也就是有些同学在一些文章上看到的“宗量”一词(当时看到宗量的时候硬是没拐过弯来,满脸问号??),根据宗量的数量不同(标识数量不同),可以分为单分派、双分派以及多分派(本文不谈多分派,因为java没有多分派)。

到这里,我相信大家对于什么是单分派和双分派已经有了初步的理解了。

那么到底怎么样才算单分派?怎么样又是双分派?莫急~我们继续往下走。

单分派

什么是单分派?

单分派的定义是:调用哪个对象(多态)的方法,在运行期确定,调用对象的哪个方法,在编译期确定。

多态是Java的语言三大特性,相信大家对这个应该是烂熟于心了。

但这里还是简单的给出多态的描述(理解的朋友可以跳过这一小部分):有一个A类以及继承于A类的B类和C类,其中A类有一个方法doSomething(),而这个方法在B类和C类中被重写了。如果另外此时有一个将类A作为入参,并在方法中调用了类A的方法doSomething(),那么在程序实际执行时将根据传入的对象的实际类型选择调用的方法。因为作为A类的子类B和C,它们都可以作为实参被传入。如果你传入的类型是A,则调用类A的doSomething(),如果是B类型,则调用B的doSomething(),其他亦然。如果还有不明白可以另外去Baidu。这里不做赘述。

解释以下前面的定义,单分派首先指除了方法名以外的标识只有一个,也就是宗量只有一个。而在Java中这个宗量就是我们要执行方法的实际对象类型,也就是我们要根据对象的具体类型来确定我们要调用哪个方法(这就是多态)。
这时候有同学要问啦,难道入参就不算标识吗?我们调用方法的时候不是还要通过入参来确定我们调用的方法吗?是的,入参也是标识,但是在单分派中,这个标识即宗量指的是在程序执行期间的标识,对于重载的同名方法虽然有不同的入参,但是对于到底调用对象的哪个重载方法是在编译期就已经根据入参类型确定下来了。

这里给出一个简单的例子

  • Parent:父类
  • Child:子类
  • Dispatch:演示分派特性类
  • Main:执行入库口类

1.Parent

/**
 * 父类
 */
public class Parent {

    public void f(){
        System.out.println("I am Parent.f()");
    }
}

2.Child

/**
 * 子类
 */
public class Child extends Parent {

    @Override
    public void f() {
        System.out.println("I am Child.f()");
    }
}

3.Dispatch

/**
 * 单分派
 */
public class Dispatch {

    /**
     * 调用哪个对象的方法,在运行期确定
     * @param p
     */
    public void dispatchFun(Parent p){
        p.f();
    }

    /**
     * 重载方法1.调用对象的哪个方法,(方法参数)在编译期确定
     * @param p
     */
    public void overloadFun(Parent p){
        System.out.println("I am overloadFun.Parent");
        p.f();
    }

    /**
     * 重载方法2.调用对象的哪个方法,(方法参数)在编译期确定
     * @param c
     */
    public void overloadFun(Child c){
        System.out.println("I am overloadFun.Child");
        c.f();
    }
}

4.Main

/**
 * 测试主类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/12/26 9:55
 */
public class Main {

    /**
     * main
     * @param args
     */
    public static void main(String[] args) {
        // 多态:调用哪个对象的方法,在运行期确定
        // 左边=静态类型,方法重载以左边为准
        // 右边=实际类型,多态以右边为准
        Parent p = new Child();
        Dispatch dispatch = new Dispatch();
        dispatch.dispatchFun(p);

        // 方法重载:调用对象的哪个方法,(方法参数)在编译期确定
        System.out.println("-------------------分割线-----------------");
        dispatch.overloadFun(p);

    }

}

5.执行结果
在这里插入图片描述
从最终执行结果看到,调用哪个对象的方法,是在运行期确定,即多态特性;代用对象的哪个方法,是在编译期确定,即方法重载。

因此,我们也能知晓Java是一种单分派语言。

总结

单分派指的是程序在调用方法时,具体调用哪个对象的方法是在运行期间根据具体的对象而决定的。这也就是多态,但对于调用对象的哪个方法则是在编译期就已经决定了,是由调用的方法名和传入参数的静态类型决定的,这主要针对重载方法。

双分派

前面提到,Java是一种能够支持双分派的单分派语言。那么这如何理解呢。

首先,我们先对Java的双分派做一个定义:调用哪个对象的方法,在运行期(多态)确定;调用对象的哪个方法(方法重载),在运行期确定。

换句话理解就是说:现在不光是执行哪个对象的方法,甚至连执行类的哪个方法都需要在运行期间来确定了,前面单分派中在编译期能够确定的被调用的方法在双分派这里已经不好使啦!本来我一个对象的静态类型是A类,那么即便是重载我也能够确定我要调的是哪个方法,但是现在,只要程序没有跑起来,我也不知道会执行哪个重载的方法。

理解了什么是双分派,那么接下来我们就聊聊这个双分派是怎么实现的呢?这就需要谈到访问者模式了。(这里简单给出了访问者模式的代码,有了解过访问者模式的应该毕竟容易理解,没有接触过的可以先看下文,如果不理解可以先去理解访问者模式后再来继续了解本文)

访问者模式是经典Gof 设计模式中,行为行设计模式的一种,它主要是用于将对象,与操作解耦的一种技术手段,两个角色

  • 访问者:操作
  • 被访问者:对象

从定义上看,比较难理解,太过于抽象了!我们通过一个案例来看。
假设在实际应用中,有这么一个需求

#1.文件处理,针对不同的文件格式,比如说:PPT、WORD、EXCEL、PDF、HTML、XML、YAML等
#2.需要解析不同类型文件,获取文件内容,转换成统一的文件格式,比如String结果内容
#3.需要将文件内容,进行压缩处理
#4.需要将不同类型文件,进行xxx处理
#5.等等,总的来说,需要根据上层业务需要,进行各种业务处理

#6.问题来了,你该如何实现呢?当然我们知道,单纯实现文件处理的功能不难,难的是如何应对业务扩展,让代码设计实现上满足开闭原则。总不能业务上加一个业务需求,又要到处修改代码吧?

面对这个业务需求场景,正好是访问者模式的应用场景了。它的两个角色

  • 访问者:操作,即业务处理(解析、压缩等)
  • 被访问者:对象,即各种类型文件
    明白了业务场景,下面来看类图和代码实现。我以PPT、和WORD类型文件为例。

类图
在这里插入图片描述

1.定义被访问者
FileVisitable

/**
 * 被访问者抽象
 */
public abstract class FileVisitable {

    protected String path;

    public FileVisitable(String path){
        this.path = path;
    }

    /**
     * 接受访问入口
     * @param visitor
     */
    public abstract void accept(FileVisitor visitor);
}

PPTFile

/**
 * PPT类型文件
 */
public class PPTFile extends FileVisitable {

    public PPTFile(String path){
        super(path);
    }

    /**
     * 接受访问入口
     *
     * @param visitor
     */
    @Override
    public void accept(FileVisitor visitor) {
        visitor.visit(this);
    }
}

WORDFile

/**
 * WORD类型文件
 */
public class WORDFile extends FileVisitable{

    public WORDFile(String path){
        super(path);
    }

    /**
     * 接受访问入口
     *
     * @param visitor
     */
    @Override
    public void accept(FileVisitor visitor) {
        visitor.visit(this);
    }
}

2.定位访问者
FileVisitor

/**
 * 访问者接口
 */
public interface FileVisitor {

    /**
     * 访问ppt类型文件
     * @param pptFile
     */
    void visit(PPTFile pptFile);

    /**
     * 访问word类型文件
     * @param wordFile
     */
    void visit(WORDFile wordFile);
}

ParseFileVisitor

/**
 * 文件内容解析访问者
 */
public class ParseFileVisitor implements FileVisitor {

    /**
     * 访问ppt类型文件
     *
     * @param pptFile
     */
    @Override
    public void visit(PPTFile pptFile) {
        System.out.println("解析文件内容,文件类型:PPTFile");
    }

    /**
     * 访问word类型文件
     *
     * @param wordFile
     */
    @Override
    public void visit(WORDFile wordFile) {
        System.out.println("解析文件内容,文件类型:WORDFile");
    }
}

3.应用

/**
 * 执行入口
 */
public class Main {

    public static void main(String[] args) {

        // 1.待处理文件列表
        List<FileVisitable> files = new ArrayList<FileVisitable>();
        files.add(new PPTFile("my.ppt"));
        files.add(new WORDFile("my.doc"));

        // 2.解析文件内容
        FileVisitor visitor = new ParseFileVisitor();
        for(FileVisitable f : files){
            f.accept(visitor);
        }
    }
}

4.执行结果
在这里插入图片描述
总结

从上述代码来看,我们通过访问者模式实现了双分派,其原理是基于Java中this关键字的运行时绑定即后期绑定。双分派在单分派的基础上为我们增加了一个宗量,这无疑是提高了程序的灵活性。但是具体访问者模式的优势和作用本文不做赘述,有兴趣的可以专门去了解访问者模式。

文章总结

单分派指的是程序在调用方法时,具体调用哪个对象的方法是在运行期间根据具体的对象而决定的。这也就是多态,但对于调用对象的哪个方法则是在编译期就已经决定了,是由调用的方法名和传入参数的静态类型决定的,这主要针对重载方法。
而双分派指的是在程序调用方法时,具体调用哪个对象的方法以及调用对象的哪个方法都是在运行期间决定的。
java本身的语言特性属于单分派,而通过访问者模式可是实现双分派。在访问者模式中,首先accept方法接受的是一个高层抽象类,因此多态可以保证,即单分派可以保证。同时由于在accept调用访问者方法时传入的参数是this,而this是运行时绑定的,即this的类型是由运行时的当前对象决定的,因此,重载的方法确定无法在编译器实现,反而要被延迟到了运行时,这就是java双分派实现的原理。
因此,java也可以被称为具有双分派的语言
假设我们使用的编程语言支持双分派,即方法重载支持运行期确定的话,只需要定义一个工具类,通过方法重载封装各业务操作,不需要在对象中再提供访问者的入口了。

本文代码取自https://mp.weixin.qq.com/s/4lrjrwCInu6wQcyR-3c2fw

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来是肖某人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值