适用人群 :Java 初学者 / 业务系统需要"公式可配置"的开发者 / 需要把数学表达式交给用户配置而不是写死在代码里的场景。
关键词 :exp4j
、公式引擎、动态计算、可配置规则、自定义函数与运算符。
1. exp4j 是什么?
exp4j 是一个轻量级的 Java 数学表达式解析与计算库。它允许你把诸如
3 * sin(x) - 2 / (x - 2)
、(a + b) * c
这样的字符串表达式"编译"为可执行对象,并在运行时给变量赋值后直接求值(得到 double
)。
典型用途
- 把业务公式做成配置:利率、税率、KPI 计算、计费规则、告警阈值等。
- 在 物联网/工业 系统里,让工艺/运维人员自己维护指标公式(例如:温度换算、百分比、速率等)。
- 数据分析/仿真里快速试验数学模型。
2. 如何引入(Maven/Gradle)
建议到 Maven Central 搜索 "exp4j" 查看最新版本号 ,下面以
0.4.x
为例(把x
换成最新)。
Maven
xml
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>0.4.x</version>
</dependency>
Gradle (Kotlin DSL)
kotlin
implementation("net.objecthunter:exp4j:0.4.x")
3. 第一个例子:3 行代码搞定动态表达式
java
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
public class QuickStart {
public static void main(String[] args) {
Expression exp = new ExpressionBuilder("3 * sin(x) - 2 / (x - 2)")
.variables("x") // 声明变量名
.build()
.setVariable("x", Math.PI / 2); // 赋值
double result = exp.evaluate();
System.out.println(result); // 输出 3 * sin(π/2) - 2/(π/2 - 2)
}
}
要点
ExpressionBuilder
用来"编译"表达式字符串。variables("x", "y", ...)
先声明用到的变量,再通过setVariable
赋值。evaluate()
返回double
。
4. 语法速查:你能写哪些表达式?
4.1 运算符(内置)
+ - * / ^
(加减乘除、幂)- 一元负号:
-x
- 括号:
( ... )
建议不要依赖隐式乘法 (如
2x
),统一写成2 * x
,更清晰也更不易踩坑。
4.2 常见函数(内置,使用弧度制)
- 三角:
sin, cos, tan, asin, acos, atan
- 取整&绝对值:
floor, ceil, abs
- 指数对数:
sqrt, ln, log
(log
为 10 为底) - 聚合:
min, max
(可多参数:max(a,b,c,...)
)
三角函数使用弧度 。如需角度,自己封装:
sind(x) = sin(x * π / 180)
(见 §6 自定义函数)。
4.3 常量
- 通常支持
pi
、e
(不同版本支持略有差异,若未识别,可自行声明变量传入)。
5. 变量赋值的几种方式
java
Expression exp = new ExpressionBuilder("(a + b) * c")
.variables("a","b","c")
.build();
exp.setVariable("a", 1.2)
.setVariable("b", 3.4)
.setVariable("c", 5);
double r1 = exp.evaluate();
// 一次性批量传 Map
Map<String, Double> vars = new HashMap<>();
vars.put("a", 2.0); vars.put("b", 2.0); vars.put("c", 10.0);
exp.setVariables(vars);
double r2 = exp.evaluate();
性能建议 :重用 已构建的 Expression
,在循环里仅改变量值 再 evaluate()
,避免重复解析表达式。
6. 自定义函数:扩展你的"数学词汇表"
假设我们想要角度制 三角函数 sind(x)
:
java
import net.objecthunter.exp4j.function.Function;
Function sind = new Function("sind", 1) {
@Override
public double apply(double... args) {
double deg = args[0];
return Math.sin(deg * Math.PI / 180.0);
}
};
Expression exp = new ExpressionBuilder("5 * sind(alpha) + 2")
.variables("alpha")
.function(sind) // 挂载自定义函数
.build()
.setVariable("alpha", 30);
System.out.println(exp.evaluate()); // 5 * sin(30°) + 2 = 5*0.5 + 2 = 4.5
再来一个条件函数 if(cond, a, b)
(把 cond>0 当作 true):
java
Function iff = new Function("iff", 3) {
@Override
public double apply(double... args) {
double cond = args[0];
return cond > 0 ? args[1] : args[2];
}
};
// 用法示例:iff( a - b , 100 , 0 ) // a>b?100:0
exp4j 本身是"纯数值"的,没有布尔类型和比较运算符;你可以用这种"条件函数"的方式做数值化判断。
7. 自定义运算符:像写数学一样扩展语法
示例:实现阶乘 运算符 !
(右结合,一元,优先级略高于幂)。
java
import net.objecthunter.exp4j.operator.Operator;
Operator factorial = new Operator("!", 1, true, Operator.PRECEDENCE_POWER + 1) {
@Override
public double apply(double... args) {
int n = (int) args[0];
if (n < 0) throw new IllegalArgumentException("n must be >= 0");
double r = 1;
for (int i = 2; i <= n; i++) r *= i;
return r;
}
};
Expression exp = new ExpressionBuilder("3! + 2^3")
.operator(factorial)
.build();
System.out.println(exp.evaluate()); // 6 + 8 = 14
参数说明
"!"
:符号1
:操作数个数true
:是否左结合precedence
:优先级(数值越大,优先级越高)
8. 实战:把"可配置公式"塞进你的业务
8.1 Spring Boot 制作"表达式计算"服务(REST API)
Controller
java
@RestController
@RequestMapping("/expr")
public class ExprController {
@PostMapping("/eval")
public Map<String, Object> eval(@RequestBody EvalReq req) {
try {
ExpressionBuilder builder = new ExpressionBuilder(req.getExpr());
if (req.getFunctions() != null) {
for (Function f : CustomFunctions.resolve(req.getFunctions())) {
builder.function(f);
}
}
Expression exp = builder
.variables(req.getVars().keySet())
.build()
.setVariables(req.getVars());
double value = exp.evaluate();
return Map.of("ok", true, "value", value);
} catch (Exception e) {
return Map.of("ok", false, "error", e.getMessage());
}
}
}
@Data
class EvalReq {
private String expr;
private Map<String, Double> vars;
private List<String> functions; // 例如 ["sind","iff"]
}
自定义函数集合(可按需扩展)
java
public class CustomFunctions {
public static Collection<Function> resolve(List<String> names) {
List<Function> fs = new ArrayList<>();
if (names == null) return fs;
for (String n : names) {
switch (n) {
case "sind":
fs.add(new Function("sind", 1) {
@Override public double apply(double... a) {
return Math.sin(a[0] * Math.PI / 180);
}
});
break;
case "iff":
fs.add(new Function("iff", 3) {
@Override public double apply(double... a) {
return a[0] > 0 ? a[1] : a[2];
}
});
break;
}
}
return fs;
}
}
调用示例(Postman/前端):
json
POST /expr/eval
{
"expr": "(avg_now - avg_prev) / avg_prev * 100",
"vars": { "avg_now": 21.0, "avg_prev": 20.0 }
}
返回:
json
{ "ok": true, "value": 5.0 }
9. 常见错误 & 排错指南
现象/异常 | 可能原因 | 解决办法 |
---|---|---|
UnknownFunctionOrVariableException |
表达式里使用了未声明的变量或函数 | 检查 variables(...) 或 .function(...) 是否漏了 |
ArithmeticException: Division by zero |
除数为 0 | 在业务里提前校验或用 iff 做保护 |
结果不对/NaN | 参数单位不一致(如角度/弧度)或变量未赋值 | 明确单位;打印传入的变量值 |
性能不佳 | 每次都 new ExpressionBuilder |
重用 Expression ,循环里只换变量值 |
多线程偶发异常 | 多线程共享同一个 Expression 且同时 setVariable |
为每个线程/请求创建独立实例,或用对象池+线程隔离 |
10. 最佳实践清单(新手照抄)
- 表达式固定,但变量常变 → 编译一次,循环
setVariable
+evaluate()
。 - 三角函数记得是弧度 → 要角度就用
sind/cosd
自定义函数。 - 别依赖隐式乘法 → 一律写
*
。 - 需要条件/比较 → 用"数值化"的
iff
函数封装,而不是写>
<
。 - 对外暴露计算接口 → 做白名单:只允许你定义的一组函数出现,拒绝其它名字。
- 日志与可观测 → 打印表达式字符串和变量 Map,方便排查线上问题。
- 单元测试 → 给每条关键公式写 2~3 组用例,保证升级/改配置不出错。
11. 一个完整可运行的 Demo(整合自定义函数 + 变量)
java
public class Exp4jDemo {
public static void main(String[] args) {
// 1) 自定义函数:角度制 sin
Function sind = new Function("sind", 1) {
@Override public double apply(double... a) {
return Math.sin(a[0] * Math.PI / 180);
}
};
// 2) 自定义条件:iff(cond, a, b)
Function iff = new Function("iff", 3) {
@Override public double apply(double... a) {
return a[0] > 0 ? a[1] : a[2];
}
};
// 3) 业务表达式:当角度>0时,用角度制的 sin 乘以系数,否则返回 0
String expr = "iff(theta, k * sind(theta), 0)";
Expression e = new ExpressionBuilder(expr)
.variables("theta", "k")
.function(sind)
.function(iff)
.build();
// 4) 赋值并计算(可在循环中反复 set 再 evaluate)
e.setVariable("theta", 30).setVariable("k", 10);
System.out.println(e.evaluate()); // 10 * sin(30°) = 5
e.setVariable("theta", 0);
System.out.println(e.evaluate()); // 0
}
}
12. 小结
- exp4j = 轻量 + 易用。适合把数学公式从代码里"抽出来",交给配置。
- 自定义函数/运算符让它非常可扩展,能覆盖 90% 的业务公式场景。
- 在工程化上,注意线程隔离、白名单函数、日志与测试,就能做成一个可靠的"公式引擎"。