☰
Interpreter 解释器模式实战解读:基于 java-design-patterns 的语法树构建与表达式求值
2026/10/1 8:08:24 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

导读

本文以开源仓库 java-design-patterns 中的 Interpreter(解释器)模式实现为骨架,完整讲解该行为型模式的核心理念、类层次设计与后缀表达式求值实战。读者将掌握如何为一个简单语言(如基础四则运算)定义语法表示(抽象语法树),并用栈式求值器逐 token 解释句子,同时了解该模式在 Java 标准库中的真实应用场景及其适用边界。

模式目的:为语言定义语法表示与解释器

给定一种语言,为它的文法(grammar)定义一个表示,同时提供一个利用该表示来解释语言中句子的解释器。

这是 Gang of Four 对 Interpreter 模式的经典定义。它属于行为型模式,核心思路是:语言中的每个符号(终结符或非终结符)对应一个类,一句话的语法树是该模式(Composite)的一个实例,客户端通过它求值(解释)整句话。在仓库中,该模式的分类信息可在 localization/es/interpreter/README.md 的 front matter 中看到(category: Behavioral,tag 含 Gang of Four)。

现实例子与通俗解释

现实例子:半身人小孩在学校学习基础数学,从最简单的算式开始:"1 + 1"、"4 - 2"、"5 + 5" 等。我们需要一个能读懂并计算出这些算式结果的"小计算器"。

通俗解释:解释器模式负责解释目标语言中的句子——把"4 3 2 - 1 + *"这样的字符串,翻译成由表达式对象组成的语法树,再递归求值得到最终结果。

Wikipedia 定义:解释器模式是一种指定如何评估语言中语句的设计模式。基本思想是让专门语言的每个符号(终结符或非终结符)对应一个类;语言中语句的语法树是组合模式(Composite)的一个实例,用于为客户端评估(解释)该语句。

类层次设计:从抽象 Expression 到具体表达式

抽象基类 Expression

要实现基础数学运算的解释,首先需要一套表达式层级。其抽象基类是Expression,声明了求值入口interpret()与便于调试和日志输出的toString():

public abstract class Expression { public abstract int interpret(); @Override public abstract String toString(); }

对应源码位于 Expression.java。整个解释器体系都建立在这两个抽象方法之上:interpret()递归求值,toString()返回运算符符号(如+、-、*),用于日志中标识当前操作。

终结符:NumberExpression

最简单的一类表达式是数字表达式,内部只保存一个整数,属于语法树中的叶子节点(终结符):

public class NumberExpression extends Expression { private final int number; public NumberExpression(int number) { this.number = number; } public NumberExpression(String s) { this.number = Integer.parseInt(s); } @Override public int interpret() { return number; } @Override public String toString() { return "number"; } }

源码见 NumberExpression.java。它提供了两个构造器:一个直接接收int,另一个接收字符串并通过Integer.parseInt(s)完成 token 到数值的转换——这正是解析阶段把"4"这类字符串变成可求值对象的关键。

非终结符:PlusExpression / MinusExpression / MultiplyExpression

更复杂的表达式是运算类,如PlusExpression、MinusExpression和MultiplyExpression。它们各持有一个左子表达式和一个右子表达式,构成语法树的内部节点(非终结符)。以加法为例:

public class PlusExpression extends Expression { private final Expression leftExpression; private final Expression rightExpression; public PlusExpression(Expression leftExpression, Expression rightExpression) { this.leftExpression = leftExpression; this.rightExpression = rightExpression; } @Override public int interpret() { return leftExpression.interpret() + rightExpression.interpret(); } @Override public String toString() { return "+"; } }

对应源码 PlusExpression.java。其余两个运算类结构完全相同,仅在interpret()中把+换成-与*,可分别参考 MinusExpression.java 与 MultiplyExpression.java。

可以清楚看到模式的精髓:求值逻辑被递归地下放到每个节点自身——PlusExpression.interpret()只负责"把左右两棵子树的结果相加",它不关心子树内部是什么。这正是"为每个符号建立一个类"的直接体现。

程序化示例:用栈解析后缀表达式

仓库的入口程序 App.java 演示了完整的解析与求值流程。App.java的类注释明确说明:本示例支持前缀(prefix)、中缀(infix)和后缀(postfix)三种记号法,示例采用后缀法——运算符跟在操作数之后。

public static void main(String[] args) { // the halfling kids are learning some basic math at school // define the math string we want to parse final var tokenString = "4 3 2 - 1 + *"; // the stack holds the parsed expressions var stack = new Stack<Expression>(); // tokenize the string and go through them one by one var tokenList = tokenString.split(" "); for (var s : tokenList) { if (isOperator(s)) { // when an operator is encountered we expect that the numbers can be popped from the top of // the stack var rightExpression = stack.pop(); var leftExpression = stack.pop(); LOGGER.info("popped from stack left: {} right: {}", leftExpression.interpret(), rightExpression.interpret()); var operator = getOperatorInstance(s, leftExpression, rightExpression); LOGGER.info("operator: {}", operator); var result = operator.interpret(); // the operation result is pushed on top of the stack var resultExpression = new NumberExpression(result); stack.push(resultExpression); LOGGER.info("push result to stack: {}", resultExpression.interpret()); } else { // numbers are pushed on top of the stack var i = new NumberExpression(s); stack.push(i); LOGGER.info("push to stack: {}", i.interpret()); } } // in the end, the final result lies on top of the stack LOGGER.info("result: {}", stack.pop().interpret()); }

两个关键辅助方法

App.java还提供了解析流程的两个支撑方法(均为public static,便于测试直接调用):

  1. isOperator(String s):判断 token 是否为运算符,当前仅识别+、-、*三种:
public static boolean isOperator(String s) { return s.equals("+") || s.equals("-") || s.equals("*"); }
  1. getOperatorInstance(String s, Expression left, Expression right):根据运算符字符串返回对应的表达式实例,使用 Java 14+ 的switch箭头语法(arrow syntax)简洁分派:
public static Expression getOperatorInstance(String s, Expression left, Expression right) { return switch (s) { case "+" -> new PlusExpression(left, right); case "-" -> new MinusExpression(left, right); default -> new MultiplyExpression(left, right); }; }

栈式求值算法逐步推导

以tokenString = "4 3 2 - 1 + *"为例,整个流程遵循经典后缀表达式求值算法:遇到数字压栈,遇到运算符则弹出栈顶两个表达式、构建运算节点并求值,再把结果作为新的NumberExpression压回栈顶。逐 token 推演如下:

步骤当前 token操作栈内容(自底向上)
14非运算符 → 压栈[4]
23非运算符 → 压栈[4, 3]
32非运算符 → 压栈[4, 3, 2]
4-弹出 3、2 → 3 - 2 = 1 → 结果压栈[4, 1]
51非运算符 → 压栈[4, 1, 1]
6+弹出 1、1 → 1 + 1 = 2 → 结果压栈[4, 2]
7*弹出 4、2 → 4 × 2 = 8 → 结果压栈[8]
8结束stack.pop().interpret()最终结果8

注意步骤 4 的弹出顺序:栈顶先弹出的是rightExpression(右操作数),再弹出leftExpression(左操作数),因此减法得到3 - 2 = 1而非2 - 3,保证了减法(以及除法等不可交换运算)的语义正确性。

运行输出

程序执行后的控制台输出(对应原文档):

popped from stack left: 1 right: 1 operator: + push result to stack: 2 popped from stack left: 4 right: 2 operator: * push result to stack: 8 result: 8

结合仓库 README.md 中的完整运行记录,可以更直观地看到每一步的日志顺序(含时间戳与类名前缀):

push to stack: 4 push to stack: 3 push to stack: 2 popped from stack left: 3 right: 2 operator: - push result to stack: 1 push to stack: 1 popped from stack left: 1 right: 1 operator: + push result to stack: 2 popped from stack left: 4 right: 2 operator: * push result to stack: 8 result: 8

两次输出相互印证:4 3 2 - 1 + *对应的中缀语义是4 * ((3 - 2) + 1),最终结果为 8。日志中的operator: +正是各运算表达式toString()的返回值,体现了toString()在调试信息中的用途。

类图

下面是该模式在仓库中的类图(来源 interpreter/etc/interpreter_1.png):

类图清晰展示了模式的核心结构:Expression为抽象根,NumberExpression为终结符叶子,三个二元运算表达式为组合型非终结符节点,语法树整体构成一个 Composite 结构。

测试佐证:表达式求值的正确性验证

仓库为每个表达式类都提供了对应的单元测试,从源码层面印证了实现行为的正确性:

  • ExpressionTest.java 是测试基类,用@ParameterizedTest参数化测试驱动求值验证;
  • NumberExpressionTest.java、PlusExpressionTest.java、MinusExpressionTest.java、MultiplyExpressionTest.java 分别为各类的测试实现。

ExpressionTest的prepareParameters(IntBinaryOperator resultCalc)方法会为两个操作数生成-10 到 10 区间内全部 20 × 20 = 400 组组合,并通过传入的二元运算函数(如(a, b) -> a + b)计算期望结果,然后断言factory.apply(first, second).interpret()与期望值相等。同时它还验证每个表达式的toString()返回值与预期符号一致(例如加法应为"+")。这一设计既验证了interpret()的数值正确性,也验证了toString()的符号契约,可作为读者扩展新运算类(如除法、取模)时的测试模板。

适用性:何时使用 Interpreter 模式

存在需要解释的语言,且语言中的语句可以表示为抽象语法树时,应考虑使用解释器模式。该模式在以下场景中效果最佳:

  • 文法足够简单。对于复杂文法,为文法建立的类层级会变得庞大而难以管理,此时诸如解析器生成器(parser generator)之类的工具是更好的选择——它们无需构建抽象语法树即可解释表达式,可以节省空间乃至时间。
  • 效率不是关键考量。最高效的解释器通常不是直接解释语法分析树,而是先将树翻译成另一种形式再执行。例如正则表达式常常被转换为状态机。但即便如此,翻译器本身仍可用解释器模式实现,因此该模式依然适用。

已知用途:Java 标准库中的解释器模式

解释器模式在 Java 生态中有大量成熟应用,以下为仓库文档列出的知名实例:

  • java.util.Pattern:正则表达式的编译与匹配,把正则文法解释为可执行的匹配状态机;
  • java.text.Normalizer:Unicode 文本规范化;
  • java.text.Format的全部子类:如SimpleDateFormat、MessageFormat等,解释日期/消息格式文法;
  • javax.el.ELResolver:统一表达式语言(Unified EL)的解析与求值。

此外,各类数据库管理系统中的 SQL 解析器也是该模式的典型应用领域。

相关模式与优缺点

相关模式:

  • Composite(组合模式):两者常常协同使用。解释器模式借助组合模式把文法表示为树形结构——Expression的递归interpret()调用正是树形组合的典型遍历方式,本仓库示例本身就是组合结构的最佳演示;
  • Flyweight(享元模式):适用于共享状态以减少内存占用,特别是解释器中存在大量重复元素(如频繁出现的数字 token)时。

优点:

  • 在不修改文法或数据类的前提下,容易新增解释表达式的操作;
  • 文法直接用语言本身实现,易于修改与扩展。

权衡:

  • 对大型文法而言可能变得复杂且低效;
  • 文法中的每条规则对应一个类,复杂文法会导致类数量激增。

小结

本文以 java-design-patterns 仓库的 interpreter 模块为完整案例,还原了 Interpreter 模式从文法定义(Expression层级)、token 解析(App.java的栈式后缀求值)到测试验证(ExpressionTest参数化测试)的全链路实现。该实现可作为读者在自研 DSL、规则引擎、计算器或查询解析器等场景中落地解释器模式的直接参考;源码与测试均可从 interpreter 模块 深入查阅。

参考资料:本文核心内容整理自 localization/es/interpreter/README.md,实现细节取自 interpreter 模块源码与测试;模式理论依据来自经典著作《Design Patterns: Elements of Reusable Object-Oriented Software》《Head First Design Patterns》与《Refactoring to Patterns》。

  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

相关推荐

上一篇:Eclipse Mosquitto版本升级指南:从1.6到2.0迁移步骤
下一篇:Processing.py终极入门指南:用Python轻松开启创意编程之旅

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询