
为什么不用 ScriptEngine 而要手写栈解析器
因为 ScriptEngine 会执行任意代码,线上环境禁用;而且它不透明、无法控制精度、报错信息模糊,比如输入 "1+2*" 只抛 ScriptException,连哪一行哪个位置错都不知道。手写栈解析器能精准定位语法错误,支持自定义函数(如 sin、log),还能无缝接入 BigDecimal 做高精度计算。
中缀转后缀(Shunting Yard)必须处理的三个细节
这是核心步骤,但网上多数示例漏掉关键边界:
- 右括号
')'遇到栈顶不是左括号时,必须报错 —— 否则"1+2))"会静默吞掉多余右括号 - 运算符优先级相同时,加减法要左结合(
1-2-3→((1-2)-3)),乘除也要左结合,但幂运算(^)得右结合(2^3^2→2^(3^2)) - 负号和减号必须区分:表达式开头的
'-'(如"-2+3")或左括号后的'-'(如"( -2 + 3 )")是负号,要转成"0-2"再入栈,否则后缀表达式会少一个操作数
Stack<String> 存什么、怎么弹,直接决定健壮性
别存字符(char),存字符串(String)—— 因为要支持多位数、小数("3.14")、函数名("sqrt")。弹栈时注意:
- 遇到操作数(数字或变量)直接进输出队列
- 遇到函数名(如
"sin")压栈,等见到'('再一起处理;函数调用结束时(即匹配到')'),要把函数名从栈里弹出来,加到输出队列末尾 - 运算符弹栈条件是:
栈非空且栈顶不是 '('且栈顶运算符优先级 ≥ 当前运算符(幂运算除外)
示例片段(简化逻辑):
立即学习“Java免费学习笔记(深入)”;
if (token.equals("(")) {
operatorStack.push(token);
} else if (token.equals(")")) {
while (!operatorStack.isEmpty() && !operatorStack.peek().equals("(")) {
output.add(operatorStack.pop());
}
if (operatorStack.isEmpty()) throw new IllegalArgumentException("Mismatched ')'");
operatorStack.pop(); // 弹掉 '('
if (!operatorStack.isEmpty() && isFunction(operatorStack.peek())) {
output.add(operatorStack.pop()); // 把函数名补到后缀末尾
}
}
后缀表达式求值时,Stack<BigDecimal> 的坑
用 Double 算 "0.1 + 0.2" 得到 0.30000000000000004,业务系统没法接受。改用 BigDecimal,但要注意:
- 构造时必须用字符串,不能用 double:
new BigDecimal("0.1")✅,new BigDecimal(0.1)❌(后者会先失真) - 除法必须指定
scale和RoundingMode,否则抛ArithmeticException;建议默认 scale=10,mode=RoundingMode.HALF_UP - 函数调用(如
sqrt)需自己实现,BigDecimal没有内置,可用牛顿迭代,别直接转double再算 —— 会丢精度
常见错误现象:"1/3+2/3" 算出 0.9999999999,就是因为除法没设舍入规则。
真正难的不是写通,而是让 "-sin(pi/2)+log10(100)" 这种混合表达式在各种括号嵌套、空格、负号场景下都稳定报错或算对 —— 所有分支都要覆盖,尤其函数参数列表的括号配对和逗号分隔。









