← 返回题目列表

访问者模式中的双分派是什么?

高频 困难 第 14 / 25 题 更新于 2026/07/28
访问者模式双分派多态方法分派

简化版

双分派是指一次操作的最终执行,取决于两个对象的运行时类型:访问者类型和元素类型。访问者模式通过 element.accept(visitor)visitor.visitXxx(this) 两次分派,让不同访问者对不同元素执行不同逻辑。

详细版

普通多态通常是单分派:调用哪个方法主要取决于接收者对象的运行时类型。

访问者模式中有两个变化维度:

  • 访问者类型不同:统计访问者、导出访问者、校验访问者。
  • 元素类型不同:PDF 文件、Word 文件、图片文件。

调用过程:

  1. element.accept(visitor):根据元素真实类型选择具体 accept
  2. visitor.visitPdfFile(this):根据访问者真实类型选择具体访问者实现。

最终执行哪个逻辑,由“具体元素类型 + 具体访问者类型”共同决定,这就是双分派的思想。

完整版教学

一、先理解单分派

在 Java 这类语言中,普通方法调用通常是单分派。比如:

Animal animal = new Dog();
animal.speak();

最终执行 Dog.speak(),取决于 animal 运行时实际是 Dog。这就是根据一个接收者对象的真实类型来分派方法。

但如果一个操作同时依赖两个对象的真实类型,单分派就不够直接。

二、访问者模式为什么需要双分派

访问者模式的问题是:不同访问者要处理不同元素。

例如:

  • 压缩访问者处理 PDF。
  • 压缩访问者处理 Word。
  • 提取文本访问者处理 PDF。
  • 提取文本访问者处理 Word。

最终逻辑不是只由元素决定,也不是只由访问者决定,而是由两者共同决定。

三、两次分派如何发生

第一次分派:

element.accept(visitor);

如果 element 实际是 PdfFile,就进入 PdfFile 的 accept。

第二次分派:

visitor.visitPdfFile(this);

如果 visitor 实际是 ExtractTextVisitor,就执行 ExtractTextVisitor 对 PDF 的处理逻辑。

于是最终方法由元素真实类型和访问者真实类型共同确定。

四、常见误区与工程判断

面试中不要把双分派讲成“调用了两次方法”这么简单。重点是两阶段分派分别解决不同维度:第一次按元素对象的运行时类型进入具体 accept;第二阶段由具体元素中 this 的静态类型选中 visit 重载,再按访问者对象的运行时类型执行实现。

如果语言支持多分派,访问者模式的必要性会下降。但在 Java 这类主流面向对象语言中,访问者模式是一种模拟双分派的经典方式。

工程中理解双分派能帮助你看懂编译器 AST 访问、规则引擎节点处理、文件资源操作等代码。

五、为什么双分派是访问者的关键

单分派语言里,方法调用通常只根据接收者的运行时类型决定,例如 employee.accept(visitor) 会根据 employee 的真实类型进入 Employee.accept。但 visitor.visit(x) 如果参数是父类引用,重载选择往往在编译期就确定了。访问者模式通过 accept 多走一步,让具体元素在自己的方法里把 this 以具体类型传给访问者,从而实现“元素真实类型 + 访问者真实类型”共同决定行为。

这就是双分派的价值:第一次分派确定具体元素,第二阶段共同确定具体元素对应的 visit 重载及具体访问者实现。没有这一步,访问者很容易退化成一堆 if (obj instanceof Employee) 判断,模式的结构意义就丢了。

面试时可以补一句工程理解:双分派不是为了炫技,而是为了把“类型选择”交给多态机制。代码看起来多绕了一层,但换来的是更明确的扩展点和更少的散落类型判断。

六、用工程约束检验答案

Java 普通虚方法调用只按接收者运行时类型分派;重载参数在编译期选择。访问者通过 element.accept(v) 先按元素运行时类型进入具体 accept,再由 v.visit(this) 选择与具体元素匹配的重载,因此常称“两次分派”。

检查项核心判断工程含义
第 1 次分派element.accept(visitor)由元素对象运行时类型决定
第 2 次分派visitor.visit(this)accept 内 this 的具体静态类型选重载,Visitor 实现再动态分派
缺少 acceptvisitor.visit(element)若变量类型是 Element,只能命中 visit(Element)

把关键关系压缩成一条可复述的路径:

Element e = new Employee();
e.accept(visitor);          // Employee.accept
visitor.visit(this);       // visit(Employee)
最终:ConcreteVisitor.visit(Employee)

Java 并不原生按两个运行时参数做多分派;访问者是用两次单分派协作得到相同的业务效果。

落地前可以再按下面 3 步复核:

  1. 先说明“第 1 次分派”的核心机制:element.accept(visitor);再交代边界:由元素对象运行时类型决定。
  2. 接着分析“第 2 次分派”:visitor.visit(this);不能遗漏对应代价或结果:accept 内 this 的具体静态类型选重载,Visitor 实现再动态分派。
  3. 最后用“缺少 accept”检查方案:visitor.visit(element);验收时确认若变量类型是 Element,只能命中 visit(Element)。

这三项构成完整判断链:先讲清第 1 次分派,再说明第 2 次分派,最后用缺少 accept检验实现是否越界。

面试中若能给出违反“若变量类型是 Element,只能命中 visit(Element)”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。

七、常见误区与追问

  • 误区:只看到“第 1 次分派”就认为方案成立。 必须同时说明核心机制“element.accept(visitor)”和工程边界“由元素对象运行时类型决定”。
  • 误区:把“第 2 次分派”当成无条件结论。 只有在“visitor.visit(this)”成立时,才能据此讨论“accept 内 this 的具体静态类型选重载,Visitor 实现再动态分派”。
  • 追问:双分派是否等于 Java 原生多分派? 不等于,Java 的重载仍由编译期参数类型决定。
  • 追问:第一次分派看哪个对象? 看接收 accept 调用的元素对象运行时类型。
  • 追问:第二次为什么能选中 Employee 重载? 因为代码位于 Employee.accept 中,此处 this 的静态类型就是 Employee。
  • 追问:只写 visit(Element) 可以吗? 可以运行,但会失去针对不同具体元素的类型化操作。
  • 追问:反射或 instanceof 能替代吗? 能实现类型判断,却把分派规则集中成条件链,扩展和类型检查通常更差。

八、加强记忆

记忆时抓住这条主线:单分派看一个对象运行时类型;双分派让操作由两个对象类型共同决定;访问者模式通过 accept 和 visit 实现双分派;它避免大量 instanceof 类型判断。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。