资讯动态

Java 17 的模式匹配,正在杀死访问者模式

发布时间:2026/8/26 1:38:45 来源:尧图企业网站定制
Java 17 的模式匹配正在杀死访问者模式访问者模式大概是 GoF 23 里最招人恨的一个。每次面试问到讲一个你实际用过的设计模式几乎没人主动提它。不是没用是用起来太痛苦——为了一个在不修改类的前提下添加操作的目标你得造一堆 Visitor 接口、实现类还要在原有类里加 accept 方法。Java 17 之后这件事变简单了。sealed class 加上 switch 的模式匹配让访问者模式的大部分使用场景有了更直接的写法。访问者模式到底在解决什么问题以一个经典的表达式求值为例。你有一个表达式树包含数字、加法、乘法三种节点。需求是给这个表达式树添加求值、打印、求导等不同操作。如果用访问者模式代码长这样java interface Expr { void accept(ExprVisitor v); }class Num implements Expr { int value; public void accept(ExprVisitor v) { v.visit(this); } }class Add implements Expr { Expr left, right; public void accept(ExprVisitor v) { v.visit(this); } }class Mul implements Expr { Expr left, right; public void accept(ExprVisitor v) { v.visit(this); } }interface ExprVisitor { void visit(Num n); void visit(Add a); void visit(Mul m); }class EvalVisitor implements ExprVisitor { int result; public void visit(Num n) { result n.value; } public void visit(Add a) { a.left.accept(this); int l result; a.right.accept(this); result l result; } public void visit(Mul m) { /类似/ } } 这段代码的问题在哪第一Expr 接口被污染了。每个节点类型都要实现 accept 方法而这个方法跟表达式本身的语义毫无关系。它之所以存在完全是为了支持访问者模式的技术机制。第二新增节点类型时要改 ExprVisitor 接口和所有实现类。这就是访问者模式最大的讽刺——它声称在不修改类的前提下添加操作代价是添加新类时必须修改所有操作。第三双分派的间接层让调试变得困难。一个求值操作在 Num、EvalVisitor、accept 之间跳来跳去调用栈深不说断点都不知道打在哪。Java 17 的替代方案Java 17 引入了两样东西sealed class密封类和 pattern matching for switchswitch 模式匹配。结合起来表达式树可以这样写java sealed interface Expr permits Num, Add, Mul {}record Num(int value) implements Expr {} record Add(Expr left, Expr right) implements Expr {} record Mul(Expr left, Expr right) implements Expr {} 没有 accept 方法没有 Visitor 接口。Expr 就是一个干净的代数数据类型。求值操作直接用 switch 表达式java int eval(Expr e) { return switch (e) { case Num(int v) - v; case Add(Expr l, Expr r) - eval(l) eval(r); case Mul(Expr l, Expr r) - eval(l) * eval(r); }; }打印操作同理java String print(Expr e) { return switch (e) { case Num(int v) - String.valueOf(v); case Add(Expr l, Expr r) - ( print(l) print(r) ); case Mul(Expr l, Expr r) - ( print(l) * print(r) ); }; }对比一下访问者模式用了 40 多行代码搭基础设施switch 模式匹配 5 行搞定。而且 eval 和 print 是两个独立函数互不干扰不需要共享一个 Visitor 接口。新增操作和新增类型哪个更痛访问者模式的设计假设是类层次结构稳定但操作经常新增。如果这个假设成立访问者模式确实有优势——新增操作只需要加一个 Visitor 实现类不用改 Expr 接口。但实际情况往往相反。类层次结构领域模型才是经常变的业务需求让新增节点类型比新增操作更频繁。比如表达式树后来要支持变量、函数调用、三元运算符。在访问者模式里每加一个节点类型所有 Visitor 都要改。switch 模式匹配的处理方式更诚实新增节点类型时编译器会检查 switch 是否覆盖了所有 case没覆盖就报错。这强制你显式处理新类型而不是靠运行时抛异常。Java 的 switch 在 sealed class 上默认是穷尽的漏了 case 编译不通过。新增操作时switch 方案只需要写一个独立函数不碰现有代码。访问者模式需要改 ExprVisitor 接口然后所有实现类都要跟着加方法。所以在类结构稳定、操作常变的理想场景下两者差不多。在类结构常变的真实场景下switch 模式匹配完胜。访问者模式还没完全死有一种场景访问者模式仍然有优势操作需要跨多个不相关的类层次结构共享状态。比如编译器的符号表解析阶段你需要遍历 AST 节点同时维护一个符号表栈、作用域链、类型环境。这些信息需要在不同节点类型的 visit 方法之间传递。访问者模式可以把这些状态封装在 Visitor 对象里天然支持跨节点共享。用 switch 模式匹配也能做到但你需要显式传递状态参数或者把状态提升到外层类。这不是做不到是代码组织方式不同。如果你需要维护复杂的状态机访问者模式的封装性还是有价值的。另一个例外是语言不支持 sealed class 和模式匹配。如果你还在用 Java 8这篇文章对你没用。但如果你在用 Java 17 或更高版本继续使用访问者模式就是在用更复杂的方式解决一个语言已经内置解决的问题。一个务实的迁移策略如果你现有的代码库里有大量访问者模式不必一次性重写。我的建议是新模块直接用 switch 模式匹配老模块在修改时逐步替换。替换的信号很明显当你发现某个 Visitor 类里大部分 visit 方法都是根据类型做不同的事而没有复杂的状态共享时这个 Visitor 就该被 switch 表达式替代了。反过来说如果一个 Visitor 维护了三个以上的共享状态字段且 visit 方法之间有大量交互保留访问者模式可能是更好的选择。设计模式不是宗教是工具。语言进化了工具也该跟着换。我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」23 个设计模式用漫画 答题的方式讲目前正在开发中。如果你觉得这类内容有意思搜一下「爪爪代码冒险记」或者等我后面的文章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价