← 返回题目列表

如何用 Java 实现访问者模式?

高频 中等 第 7 / 25 题 更新于 2026/07/28
访问者模式Java实现代码实现

简化版

Java 实现访问者模式通常先定义元素接口 accept(Visitor),再定义访问者接口 visitXxx(ConcreteElement),具体元素在 accept 中调用访问者方法,具体访问者实现不同操作。

详细版

实现步骤:

  1. 定义 Visitor 接口,为每类元素声明 visit 方法。
  2. 定义 Element 接口,声明 accept 方法。
  3. 每个 ConcreteElement 实现 accept,并调用对应 visit 方法。
  4. 定义 ConcreteVisitor,实现某种具体操作。
  5. 对象结构遍历元素并调用 accept。

示例场景:文件资源处理。

interface ResourceFile {
    void accept(Visitor visitor);
}

interface Visitor {
    void visitPdf(PdfFile file);
    void visitWord(WordFile file);
}

核心代码在元素:

class PdfFile implements ResourceFile {
    public void accept(Visitor visitor) {
        visitor.visitPdf(this);
    }
}

完整版教学

一、先定义访问者接口

访问者接口要体现“能访问哪些元素”:

interface Visitor {
    void visitPdf(PdfFile file);
    void visitWord(WordFile file);
}

这里每一个 visit 方法都对应一种具体元素类型。如果元素类型稳定,这个接口也会相对稳定。

二、定义元素接口和具体元素

interface ResourceFile {
    void accept(Visitor visitor);
}

class PdfFile implements ResourceFile {
    private String path;

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

    public String getPath() {
        return path;
    }

    public void accept(Visitor visitor) {
        visitor.visitPdf(this);
    }
}

accept 里传入 this,让访问者拿到具体类型。

三、定义具体访问者

class ExtractTextVisitor implements Visitor {
    public void visitPdf(PdfFile file) {
        System.out.println("提取 PDF 文本:" + file.getPath());
    }

    public void visitWord(WordFile file) {
        System.out.println("提取 Word 文本:" + file.getPath());
    }
}

如果以后新增压缩操作,只需要新增 CompressVisitor,不必修改 PdfFile 和 WordFile。

四、常见误区与工程判断

代码实现中最容易漏的是:具体元素的 accept 不能写成统一的 visitor.visit(this),除非语言或接口设计支持对应重载并能正确分派。Java 的重载选择在编译期决定,访问者模式通常需要明确写出 visitPdf(this) 这类方法。

工程中如果访问者越来越多,可以按业务模块组织访问者,避免所有访问者堆在同一个包里。访问者模式解决操作扩展,但不解决代码组织混乱,需要配合包结构管理。

五、代码实现时最该注意的细节

Java 实现访问者模式时,最容易写错的是把分派逻辑写回 visitor 里,例如在 visit(Element e) 中用 instanceof 判断类型。这样虽然能跑,但本质上没有发挥访问者模式的多态分派优势。更规范的写法是 Visitor 接口为每个具体元素提供明确的 visitXxx 方法,具体元素在 accept 中传入 this

第二个细节是 Visitor 接口的稳定性。只要新增元素类型,Visitor 接口就要变化,所有实现类都要补方法。因此在真实项目里,访问者适合元素类型经过建模后比较稳定的结构。如果元素类型还在频繁探索期,过早上访问者会导致大量联动修改。

第三个细节是返回值和上下文。示例里常写成 void visitXxx,但工程里经常需要返回结果或累计状态。可以让 visitor 内部保存累计值,也可以把 visit 设计成带返回值的泛型接口。选择哪种方式取决于操作是否有共享状态、是否需要线程安全、是否希望 visitor 可复用。

六、用工程约束检验答案

Java 实现的验收点不是类名齐全,而是 Employee.accept 必须调用 visitor.visit(this)Department.accept 也必须传自己的 this。用 2 种元素和 2 个访问者测试,应覆盖 4 个“访问者 × 元素”组合,才能证明分派完整。

检查项核心判断工程含义
Element.accept只接收 Visitor不包含报表或审计细节
Visitor 重载参数使用具体元素类型获得编译期类型检查
ConcreteVisitor实现完整 visit 集合一种操作集中在一处

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

2 elements x 2 visitors = 4 checks
Employee -> SalaryVisitor / AuditVisitor
Department -> SalaryVisitor / AuditVisitor
每条路径断言独立结果

若 accept 中出现一长串 instanceof,说明示例没有真正利用访问者的分派结构。

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

  1. 先说明“Element.accept”的核心机制:只接收 Visitor;再交代边界:不包含报表或审计细节。
  2. 接着分析“Visitor 重载”:参数使用具体元素类型;不能遗漏对应代价或结果:获得编译期类型检查。
  3. 最后用“ConcreteVisitor”检查方案:实现完整 visit 集合;验收时确认一种操作集中在一处。

这三项构成完整判断链:先讲清Element.accept,再说明Visitor 重载,最后用ConcreteVisitor检验实现是否越界。

面试中若能给出违反“一种操作集中在一处”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。

七、常见误区与追问

  • 误区:只看到“Element.accept”就认为方案成立。 必须同时说明核心机制“只接收 Visitor”和工程边界“不包含报表或审计细节”。
  • 误区:把“Visitor 重载”当成无条件结论。 只有在“参数使用具体元素类型”成立时,才能据此讨论“获得编译期类型检查”。
  • 追问:Visitor 接口能否用泛型返回值? 可以设计 Visitor<R>,但不同 visit 返回类型需统一为 R。
  • 追问:accept 能否返回结果? 可以返回泛型结果,也可让访问者累积结果;应按调用语义选择。
  • 追问:访问者实例能否做单例? 无状态访问者可以,有累计字段的访问者则要考虑并发和污染。
  • 追问:是否必须把所有类设为 public? 不必,可利用包可见性收紧 Memento 类似的协作边界。
  • 追问:怎样测试新增元素的影响? 编译器会提示所有未实现的新 visit 方法,再为每个访问者补行为测试。

八、加强记忆

记忆时抓住这条主线:Java 实现访问者要有 Visitor 和 Element 两类接口;元素在 accept 中调用具体 visit 方法;新增操作就是新增 ConcreteVisitor;Java 重载分派细节要特别注意。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。