Java 中的解释器模式:为 Java 应用程序构建自定义解析器
解释器设计模式的意图
解释器设计模式用于定义一种语言的语法表示,并提供一个解释器来处理这种语法。该模式在需要解释和执行特定规则或语法的情况下很有用,例如算术表达式或脚本语言。
解释器模式的详细解释及现实世界中的示例
现实世界中的示例
假设一个计算器应用程序被设计用来解释和计算用户输入的表达式。该应用程序使用 Java 中的解释器模式来解析和评估算术表达式,例如“5 + 3 * 2”。在这里,解释器将表达式的每个部分转换为表示数字和运算的对象。这些对象遵循一种定义的语法,使应用程序能够根据算术规则正确理解和计算结果。表达式的每个元素对应于程序结构中的一个类,简化了对任何输入算术公式的解析和评估过程。
用简单的话说
解释器设计模式定义了一种语言语法的表示,以及一个使用该表示来解释该语言中句子的解释器。
维基百科说
在计算机编程中,解释器模式是一种设计模式,它指定如何评估语言中的句子。基本思想是为专门的计算机语言中的每个符号(终结符或非终结符)创建一个类。语言中句子的语法树是组合模式的实例,用于评估(解释)客户端的句子。
Java 中解释器模式的编程示例
为了能够在 Java 中解释基本的数学运算,我们需要一个表达式层次结构。Expression
类是基类,具体实现(如 NumberExpression
)处理语法的特定部分。Java 中的解释器模式通过将算术表达式转换为应用程序可以处理的结构化格式,简化了解析和评估算术表达式。
public abstract class Expression {
public abstract int interpret();
@Override
public abstract String 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";
}
}
更复杂的表达式是运算,例如 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 "+";
}
}
现在,我们能够展示解释器模式在解析一些简单数学运算时的实际操作。
@Slf4j
public class App {
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());
}
public static boolean isOperator(String s) {
return s.equals("+") || s.equals("-") || s.equals("*");
}
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);
};
}
}
执行程序会产生以下控制台输出。
13:33:15.437 [main] INFO com.iluwatar.interpreter.App -- push to stack: 4
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push to stack: 3
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push to stack: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- popped from stack left: 3 right: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- operator: -
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push result to stack: 1
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push to stack: 1
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- popped from stack left: 1 right: 1
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- operator: +
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push result to stack: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- popped from stack left: 4 right: 2
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- operator: *
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- push result to stack: 8
13:33:15.440 [main] INFO com.iluwatar.interpreter.App -- result: 8
解释器模式的详细解释及现实世界中的示例
何时在 Java 中使用解释器模式
当需要解释一种语言,并且可以将语言中的语句表示为抽象语法树时,使用解释器设计模式。解释器模式在以下情况下最有效
- 语法很简单。对于复杂的语法,语法的类层次结构会变得很大且难以管理。在这种情况下,解析器生成器之类的工具是更好的选择。它们可以解释表达式,而无需构建抽象语法树,这可以节省空间,并且可能节省时间。
- 效率不是关键问题。最有效的解释器通常不是通过直接解释解析树来实现的,而是首先将它们转换为另一种形式。例如,正则表达式通常被转换为状态机。但即使如此,转换器也可以通过解释器模式来实现,因此该模式仍然适用。
解释器模式在 Java 中的现实世界应用
- java.util.Pattern
- java.text.Normalizer
- java.text.Format 的所有子类
- javax.el.ELResolver
- 各种数据库管理系统中的 SQL 解析器。
解释器模式的优缺点
优点
- 轻松地添加新的操作来解释表达式,而无需更改语法或数据类。
- 直接在语言中实现语法,使其易于修改或扩展。
权衡
- 对于大型语法可能会变得复杂且效率低下。
- 语法的每个规则都会导致一个类,对于复杂的语法,会导致大量类。