30 分钟上手 exp4j:在 Java 中安全、灵活地计算数学表达式

适用人群 :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, loglog 为 10 为底)
  • 聚合:min, max(可多参数:max(a,b,c,...)

三角函数使用弧度 。如需角度,自己封装:sind(x) = sin(x * π / 180)(见 §6 自定义函数)。

4.3 常量

  • 通常支持 pie(不同版本支持略有差异,若未识别,可自行声明变量传入)。

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. 最佳实践清单(新手照抄)

  1. 表达式固定,但变量常变 → 编译一次,循环 setVariable + evaluate()
  2. 三角函数记得是弧度 → 要角度就用 sind/cosd 自定义函数。
  3. 别依赖隐式乘法 → 一律写 *
  4. 需要条件/比较 → 用"数值化"的 iff 函数封装,而不是写 > <
  5. 对外暴露计算接口 → 做白名单:只允许你定义的一组函数出现,拒绝其它名字。
  6. 日志与可观测 → 打印表达式字符串和变量 Map,方便排查线上问题。
  7. 单元测试 → 给每条关键公式写 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% 的业务公式场景。
  • 在工程化上,注意线程隔离、白名单函数、日志与测试,就能做成一个可靠的"公式引擎"。

相关推荐
馨谙4 小时前
SSH密钥认证:从密码到密钥的安全升级指南
运维·安全·ssh
郝学胜-神的一滴4 小时前
Linux 进程控制块(PCB)解析:深入理解进程管理机制
linux·服务器·开发语言
后端小张4 小时前
【鸿蒙开发手册】重生之我要学习鸿蒙HarmonyOS开发
开发语言·学习·华为·架构·harmonyos·鸿蒙·鸿蒙系统
胖咕噜的稞达鸭4 小时前
AVL树手撕,超详细图文详解
c语言·开发语言·数据结构·c++·算法·visual studio
张较瘦_4 小时前
环境搭建 | [入门级]VSCode(Cursor|Trae|Qoder)搭建Java(Springboot3)企业开发环境全流程
java·ide·vscode
007php0074 小时前
百度面试题解析:synchronized、volatile、JMM内存模型、JVM运行时区域及堆和方法区(三)
java·开发语言·jvm·缓存·面试·golang·php
YSRM4 小时前
Leetcode+Java+图论II
java·leetcode·图论
十铭忘4 小时前
基于SAM2的眼动数据跟踪2
java·服务器·前端
okjohn4 小时前
浅谈需求分析与管理
java·架构·系统架构·软件工程·产品经理·需求分析·规格说明书