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

  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% 的业务公式场景。
  • 在工程化上,注意线程隔离、白名单函数、日志与测试,就能做成一个可靠的"公式引擎"。

相关推荐
大黄说说3 分钟前
PHP 异步怎么实现?Swoole、Workerman 使用场景对比
开发语言·php·swoole
十二同学啊4 分钟前
Python 工程化详解:项目结构、依赖管理、代码质量、测试与部署
java·开发语言·python
YIN_尹8 分钟前
【git原理与使用】git基本操作及底层原理(一)
java·git·elasticsearch
乐橙开放平台11 分钟前
跨地域监控难管理?通过 GB28181 把各地海康大华乐橙摄像头注册到云端平台
开发语言·笔记·物联网·自动化·php·音视频·智能家居
广东融科数据服务有限公司12 分钟前
网络监控经常卡顿、掉线、画面模糊?一线排查问答实录
开发语言·网络·php
有志冬瓜17 分钟前
.NET Framework全部版本离线下载安装教程
安全·.net·软件需求
程序员飞哥18 分钟前
AI 编程额度怎么用才不浪费?
java·前端·后端
yh弓长23 分钟前
Spring MVC 的请求链路
java·spring·mvc
拆房老料24 分钟前
【第四部分:Agentic 智能体设计模式】17. Agent 路由模式详解:从意图识别到动态模型选择
java·设计模式·ai编程·策略模式
不在逃避q24 分钟前
Chrome 扩展安全攻防实录:从攻击面到防御实践
前端·chrome·安全