- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
导读
本文以开源仓库 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,便于测试直接调用):
isOperator(String s):判断 token 是否为运算符,当前仅识别+、-、*三种:
public static boolean isOperator(String s) { return s.equals("+") || s.equals("-") || s.equals("*"); }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 | 操作 | 栈内容(自底向上) |
|---|---|---|---|
| 1 | 4 | 非运算符 → 压栈 | [4] |
| 2 | 3 | 非运算符 → 压栈 | [4, 3] |
| 3 | 2 | 非运算符 → 压栈 | [4, 3, 2] |
| 4 | - | 弹出 3、2 → 3 - 2 = 1 → 结果压栈 | [4, 1] |
| 5 | 1 | 非运算符 → 压栈 | [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
相关推荐
Java 设计模式之解释器模式(Interpreter):用 java-design-patterns 构建自定义算术表达式解析器
Java 设计模式之解释器模式(Interpreter):用 java design patterns 构建自定义算术表达式解析器 解释器模式(Interpre
示例工程教程DesignPatternsPHP 解释器模式(Interpreter Pattern)实战:用 PHP 8 类构建二进制逻辑表达式语法树
DesignPatternsPHP 解释器模式(Interpreter Pattern)实战:用 PHP 8 类构建二进制逻辑表达式语法树 解释器模式(Inte
示例工程教程Java 设计模式实战:Composite(组合)模式构建部分-整体树形结构(java-design-patterns 仓库解读)
Java 设计模式实战:Composite(组合)模式构建部分 整体树形结构(java design patterns 仓库解读) 本篇技术指南以 java d
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考