写点什么

这个无敌设计,可以解析并运算任意数学表达式

作者:Tom弹架构
  • 2021 年 11 月 18 日
  • 本文字数:6350 字

    阅读完需:约 21 分钟

本文节选自《设计模式就该这样学》

1 使用解释器模式解析数学表达式

下面用解释器模式来实现一个数学表达式计算器,包含加、减、乘、除运算。首先定义抽象表达式角色 IArithmeticInterpreter 接口。



public interface IArithmeticInterpreter { int interpret();}
复制代码


创建终结表达式角色 Interpreter 抽象类。



public abstract class Interpreter implements IArithmeticInterpreter {
protected IArithmeticInterpreter left; protected IArithmeticInterpreter right;
public Interpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) { this.left = left; this.right = right; }}
复制代码


然后分别创建非终结符表达式角色加、减、乘、除解释器,加法运算表达式 AddInterpreter 类的代码如下。



public class AddInterpreter extends Interpreter {
public AddInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) { super(left, right); }
public int interpret() { return this.left.interpret() + this.right.interpret(); }}
复制代码


减法运算表达式 SubInterpreter 类的代码如下。



public class SubInterpreter extends Interpreter { public SubInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) { super(left, right); }
public int interpret() { return this.left.interpret() - this.right.interpret(); }}
复制代码


乘法运算表达式 MultiInterpreter 类的代码如下。



public class MultiInterpreter extends Interpreter {
public MultiInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right){ super(left,right); }
public int interpret() { return this.left.interpret() * this.right.interpret(); }
}
复制代码


除法运算表达式 DivInterpreter 类的代码如下。



public class DivInterpreter extends Interpreter {
public DivInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right){ super(left,right); }
public int interpret() { return this.left.interpret() / this.right.interpret(); }
}
复制代码


数字表达式 NumInterpreter 类的代码如下。



public class NumInterpreter implements IArithmeticInterpreter { private int value;
public NumInterpreter(int value) { this.value = value; }

public int interpret() { return this.value; }}
复制代码


接着创建计算器 GPCalculator 类。



public class GPCalculator { private Stack<IArithmeticInterpreter> stack = new Stack<IArithmeticInterpreter>();
public GPCalculator(String expression) { this.parse(expression); }
private void parse(String expression) { String [] elements = expression.split(" "); IArithmeticInterpreter left,right;
for (int i = 0; i < elements.length ; i++) { String operator = elements[i]; if(OperatorUtil.ifOperator(operator)){ left = this.stack.pop(); right = new NumInterpreter(Integer.valueOf(elements[++i])); System.out.println("出栈" + left.interpret() + "和" + right.interpret()); this.stack.push(OperatorUtil.getInterpreter(left,right,operator)); System.out.println("应用运算符:" + operator); }else { NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(elements[i])); this.stack.push(numInterpreter); System.out.println("入栈:" + numInterpreter.interpret()); }
} }
public int calculate() { return this.stack.pop().interpret(); }}
复制代码


工具类 OperatorUtil 的代码如下。



public class OperatorUtil {
public static boolean isOperator(String symbol) { return (symbol.equals("+") || symbol.equals("-") || symbol.equals("*")); }
public static Interpreter getInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right, String symbol) { if (symbol.equals("+")) { return new AddInterpreter(left, right); } else if (symbol.equals("-")) { return new SubInterpreter(left, right); } else if (symbol.equals("*")) { return new MultiInterpreter(left, right); } else if (symbol.equals("/")) { return new DivInterpreter(left, right); } return null; }}
复制代码


最后编写客户端测试代码。



public static void main(String[] args) { System.out.println("result: " + new GPCalculator("10 + 30").calculate()); System.out.println("result: " + new GPCalculator("10 + 30 - 20").calculate()); System.out.println("result: " + new GPCalculator("100 * 2 + 400 * 1 + 66").calculate());}
复制代码


运行结果如下图所示。



当然,上面的简易计算器还没有考虑优先级,就是从左至右依次运算的。在实际运算中,乘法和除法属于一级运算,加法和减法属于二级运算。一级运算需要优先计算。另外,我们可以通过使用括号手动调整运算的优先级。我们再优化一下代码,首先新建一个枚举类。



public enum OperatorEnum { LEFT_BRACKET("("), RIGHT_BRACKET(")"), SUB("-"), ADD("+"), MULTI("*"), DIV("/"), ; private String operator;
public String getOperator() { return operator; }
OperatorEnum(String operator) { this.operator = operator; }}
复制代码


然后修改 OperatorUtil 的处理逻辑,设置两个栈。



public class OperatorUtil {
public static Interpreter getInterpreter(Stack<IArithmeticInterpreter> numStack, Stack<String> operatorStack) { IArithmeticInterpreter right = numStack.pop(); IArithmeticInterpreter left = numStack.pop(); String symbol = operatorStack.pop(); System.out.println("数字出栈:" + right.interpret() + "," + left.interpret() + ",操作符出栈:" + symbol); if (symbol.equals("+")) { return new AddInterpreter(left, right); } else if (symbol.equals("-")) { return new SubInterpreter(left, right); } else if (symbol.equals("*")) { return new MultiInterpreter(left, right); } else if (symbol.equals("/")) { return new DivInterpreter(left, right); } return null; }}
复制代码


修改 GPCalculator 的代码。



public class GPCalculator {
//数字stack private Stack<IArithmeticInterpreter> numStack = new Stack<IArithmeticInterpreter>(); //操作符stack private Stack<String> operatorStack = new Stack<String>(); /** * 解析表达式 * @param expression */ public GPCalculator(String expression) { this.parse(expression); }
private void parse(String input) { //对表达式去除空字符操作 String expression = this.fromat(input); System.out.println("标准表达式:" + expression); for (String s : expression.split(" ")) { if (s.length() == 0){ //如果是空格,则继续循环,什么也不操作 continue; } //如果是加减,因为加减的优先级最低,所以这里只要遇到加减号,无论操作符栈中是什么运算符都要运算 else if (s.equals(OperatorEnum.ADD.getOperator()) || s.equals(OperatorEnum.SUB.getOperator())) { //当栈不是空的,并且栈中最上面的一个元素是加减乘除的任意一个 while (!operatorStack.isEmpty() &&(operatorStack.peek().equals(OperatorEnum.SUB.getOperator()) || operatorStack.peek().equals(OperatorEnum.ADD.getOperator()) || operatorStack.peek().equals(OperatorEnum.MULTI.getOperator()) || operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) { //结果存入栈中 numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack)); } //运算完后将当前的运算符入栈 System.out.println("操作符入栈:"+s); operatorStack.push(s); } //当前运算符是乘除的时候,因为优先级高于加减 //所以要判断最上面的是否是乘除,如果是乘除,则运算,否则直接入栈 else if (s.equals(OperatorEnum.MULTI.getOperator()) || s.equals(OperatorEnum.DIV.getOperator())) { while (!operatorStack.isEmpty()&&( operatorStack.peek().equals(OperatorEnum.MULTI.getOperator()) || operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) { numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack)); } //将当前操作符入栈 System.out.println("操作符入栈:"+s); operatorStack.push(s); } //如果是左括号,则直接入栈,什么也不用操作,trim()函数是用来去除空格的,由于上面的分割 操作,可能会令操作符带有空格 else if (s.equals(OperatorEnum.LEFT_BRACKET.getOperator())) { System.out.println("操作符入栈:"+s); operatorStack.push(OperatorEnum.LEFT_BRACKET.getOperator()); } //如果是右括号,则清除栈中的运算符直至左括号 else if (s.equals(OperatorEnum.RIGHT_BRACKET.getOperator())) { while (!OperatorEnum.LEFT_BRACKET.getOperator().equals(operatorStack.peek())) { //开始运算 numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack)); } //运算完之后清除左括号 String pop = operatorStack.pop(); System.out.println("括号运算操作完成,清除栈中右括号:"+pop); } //如果是数字,则直接入数据的栈 else { //将数字字符串转换成数字,然后存入栈中 NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(s)); System.out.println("数字入栈:"+s); numStack.push(numInterpreter); } } //最后当栈中不是空的时候继续运算,直到栈为空即可 while (!operatorStack.isEmpty()) { numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack)); } }
/** * 计算结果出栈 * @return */ public int calculate() { return this.numStack.pop().interpret(); }
/** * 换成标准形式,便于分割 * @param expression * @return */ private String fromat(String expression) { String result = ""; for (int i = 0; i < expression.length(); i++) { if (expression.charAt(i) == '(' || expression.charAt(i) == ')' || expression.charAt(i) == '+' || expression.charAt(i) == '-' || expression.charAt(i) == '*' || expression.charAt(i) == '/') //在操作符与数字之间增加一个空格 result += (" " + expression.charAt(i) + " "); else result += expression.charAt(i); } return result; }}
复制代码


此时,再来看客户端测试代码。



public static void main(String[] args) { System.out.println("result: " + new GPCalculator("10+30/((6-4)*2-2)").calculate());}
复制代码


运行得到预期的结果,如下图所示。


2 解释器模式在 JDK 源码中的应用

先来看 JDK 源码中的 Pattern 对正则表达式的编译和解析。



public final class Pattern implements java.io.Serializable { ... private Pattern(String p, int f) { pattern = p; flags = f;
if ((flags & UNICODE_CHARACTER_CLASS) != 0) flags |= UNICODE_CASE;
capturingGroupCount = 1; localCount = 0;
if (pattern.length() > 0) { compile(); } else { root = new Start(lastAccept); matchRoot = lastAccept; } } ... public static Pattern compile(String regex) { return new Pattern(regex, 0); } public static Pattern compile(String regex, int flags) { return new Pattern(regex, flags); }
...}
复制代码

3 解释器模式在 Spring 源码中的应用

再来看 Spring 中的 ExpressionParser 接口。



public interface ExpressionParser {
Expression parseExpression(String expressionString) throws ParseException;
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}
复制代码


这里我们不深入讲解源码,通过我们前面编写的案例大致能够清楚其原理。不妨编写一段客户端代码验证一下。客户端测试代码如下。



public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("100 * 2 + 400 * 1 + 66"); int result = (Integer) expression.getValue(); System.out.println("计算结果是:" + result); }
复制代码


运行结果如下图所示。



由上图可知,运行结果与预期的结果是一致的。


关注微信公众号『 Tom 弹架构 』回复“设计模式”可获取完整源码。


【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦


本文为“Tom 弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!

如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注微信公众号『 Tom 弹架构 』可获取更多技术干货!

发布于: 2 小时前阅读数: 6
用户头像

Tom弹架构

关注

不只做一个技术者,更要做一个思考者 2021.10.22 加入

畅销书作者,代表作品: 《Spring 5核心原理与30个类手写实战》 《Netty 4核心原理与手写RPC框架实战》 《设计模式就该这样学》

评论

发布
暂无评论
这个无敌设计,可以解析并运算任意数学表达式