如何用 Java 实现解释器模式?
简化版
Java 实现解释器模式通常定义 Expression 接口和 interpret(Context) 方法,再实现变量表达式、常量表达式、与或非等组合表达式。客户端构建表达式树后,传入上下文解释得到结果。
详细版
示例:布尔规则表达式。
interface Expression {
boolean interpret(Context context);
}
变量表达式:
class VariableExpression implements Expression {
private String name;
public boolean interpret(Context context) {
return context.getBoolean(name);
}
}
与表达式:
class AndExpression implements Expression {
private Expression left;
private Expression right;
public boolean interpret(Context context) {
return left.interpret(context) && right.interpret(context);
}
}
客户端组合表达式树,再传入 Context 执行。
完整版教学
一、定义统一接口
统一接口是表达式树能递归组合的基础:
interface Expression {
boolean interpret(Context context);
}
无论是变量、常量、与、或、非,都可以当成 Expression 使用。
二、实现终结符表达式
终结符通常不包含子表达式:
class BoolExpression implements Expression {
private final boolean value;
public boolean interpret(Context context) {
return value;
}
}
变量表达式则从 Context 中取值。
三、实现非终结符表达式
非终结符组合其他表达式:
class OrExpression implements Expression {
private final Expression left;
private final Expression right;
public boolean interpret(Context context) {
return left.interpret(context) || right.interpret(context);
}
}
解释时会递归调用子表达式。
四、常见误区与工程判断
代码实现里最容易被忽略的是“字符串如何变成表达式树”。如果规则写死在代码里,示例很简单;如果规则来自配置,就需要解析器。
工程中表达式解析、校验、解释应该分层:解析负责语法,校验负责合法性,解释负责执行。混在一起会很难维护。
五、Java 示例要补上解析阶段的意识
Java 示例通常手动写 new AndExpression(a, b),这是为了展示模式结构。但工程里规则大多来自配置、数据库或用户输入,例如 "age > 18 && vip == true"。这时不能只写 Expression 类,还要有 parser 把字符串转换成表达式树。
实现时可以分层:Tokenizer 负责把字符串拆成 token,Parser 负责根据优先级和括号构建表达式树,Expression 负责解释执行,Context 负责提供变量值。这样每层职责清楚,出错时也容易定位是词法问题、语法问题还是运行时变量缺失。
如果面试只是要求写简单代码,可以写变量、常量、And、Or、Not 几个类;如果追问工程落地,就补充解析、异常处理、短路求值、类型校验和缓存表达式树。尤其是缓存很重要:同一条规则重复执行时,不应该每次都重新解析字符串。
六、用工程约束检验答案
Java 示例应区分构树与求值:new Add(new Number(1), new Multiply(new Number(2), new Number(3))) 的 interpret 结果必须为 7。若直接按字符串从左到右计算得到 9,说明解析阶段没有实现乘法优先级。
| 检查项 | 核心判断 | 工程含义 |
|---|---|---|
| Expression | 统一返回类型 R | 让组合节点静态检查 |
| 不可变节点 | final 子表达式 | AST 可安全复用 |
| Parser | token 转 AST | 与 interpret 职责分离 |
把关键关系压缩成一条可复述的路径:
Expression<Integer> ast =
Add(Number(1), Multiply(Number(2), Number(3)))
ast.interpret(context)
=> 1 + (2 * 3) = 7
演示代码手工 new AST 可以说明模式结构,但必须主动指出真实输入仍需要可靠 Parser。
落地前可以再按下面 3 步复核:
- 先说明“Expression
”的核心机制:统一返回类型 R;再交代边界:让组合节点静态检查。 - 接着分析“不可变节点”:final 子表达式;不能遗漏对应代价或结果:AST 可安全复用。
- 最后用“Parser”检查方案:token 转 AST;验收时确认与 interpret 职责分离。
这三项构成完整判断链:先讲清Expression
面试中若能给出违反“与 interpret 职责分离”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。
七、常见误区与追问
- 误区:只看到“Expression
”就认为方案成立。 必须同时说明核心机制“统一返回类型 R”和工程边界“让组合节点静态检查”。 - 误区:把“不可变节点”当成无条件结论。 只有在“final 子表达式”成立时,才能据此讨论“AST 可安全复用”。
- 追问:接口返回 Object 好吗? 灵活但丢失类型安全,单一结果域优先使用泛型。
- 追问:节点为何推荐不可变? 不可变 AST 易缓存、共享和并发求值。
- 追问:除零在哪里处理? DivideExpression 解释时检测并返回领域错误或抛出明确异常。
- 追问:如何实现短路 AND? 先解释左节点,若为 false 就不解释右节点。
- 追问:如何测试优先级? 用
1+2*3=7、(1+2)*3=9等成对用例测试 Parser 与解释器。
八、加强记忆
记忆时抓住这条主线:Expression 统一解释接口;终结符处理基本单元;非终结符递归组合子表达式;真正工程难点常在解析表达式字符串。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。