用了BigDecimal还差一分?用四组Java代码查清原因

两行金额都是0.105元,分别保留两位再相加是0.22元,先相加再保留两位却是0.21元。这两种写法都用了BigDecimal,差的一分也可能来自舍入顺序。

下面用JDK 21.0.11对照四组自建数据,分别检查double构造、逐行与合计舍入、数值比较和除法。先找出差额出现在哪一步,再核对对应业务规则;这里没有使用真实账单。

先区分两种不同的差额

第一种是输入已经近似。new BigDecimal(2.675)接收到的是double实际保存的值;它不等于直接解析十进制文本"2.675"。本例保留两位、使用HALF_UP时,前者得到2.67,后者得到2.68。

BigDecimal的double构造方法文档专门说明了这一点。若上游输入是十进制文本,应保留它,不先转换为double再期望恢复原精度。JDK 21 BigDecimal文档

第二种是计算规则不同。两行0.105,逐行舍入再合计为0.22,合计后舍入为0.21。这一组从输入到运算都使用十进制字符串构造,没有经过double。

要选哪条路径,由业务规则决定。代码只能准确执行已经确定的规则,不能替你决定明细与总计应该如何对应。

用一个完整程序留下反例

保存为MoneyRoundingDemo.java:

shell 复制代码
javac -encoding UTF-8 --release 21 MoneyRoundingDemo.java
java MoneyRoundingDemo
java 复制代码
import java.math.BigDecimal;
import java.math.RoundingMode;

public class MoneyRoundingDemo {
    static BigDecimal cents(BigDecimal value) { return value.setScale(2, RoundingMode.HALF_UP); }
    static void equal(String expected, BigDecimal actual) {
        if (!actual.toPlainString().equals(expected)) throw new AssertionError(actual);
    }
    public static void main(String[] args) {
        BigDecimal fromDouble = cents(new BigDecimal(2.675));
        BigDecimal fromText = cents(new BigDecimal("2.675"));
        equal("2.67", fromDouble); equal("2.68", fromText);
        System.out.println("double构造后舍入=" + fromDouble + ", 字符串构造后舍入=" + fromText);
        BigDecimal price = new BigDecimal("0.105");
        BigDecimal lineFirst = cents(price).add(cents(price));
        BigDecimal totalFirst = cents(price.add(price));
        equal("0.22", lineFirst); equal("0.21", totalFirst);
        System.out.println("逐行舍入再合计=" + lineFirst + ", 合计后舍入=" + totalFirst);
        BigDecimal a = new BigDecimal("1.0"), b = new BigDecimal("1.00");
        if (a.equals(b) || a.compareTo(b) != 0) throw new AssertionError();
        System.out.println("1.0.equals(1.00)=" + a.equals(b) + ", compareTo=" + a.compareTo(b));
        boolean rejected = false;
        try { BigDecimal.ONE.divide(new BigDecimal("3")); }
        catch (ArithmeticException expected) { rejected = true; }
        if (!rejected) throw new AssertionError();
        BigDecimal rounded = BigDecimal.ONE.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
        equal("0.33", rounded);
        System.out.println("1/3不指定精度会报错=" + rejected + ", 指定2位结果=" + rounded);
    }
}

实际输出:

text 复制代码
double构造后舍入=2.67, 字符串构造后舍入=2.68
逐行舍入再合计=0.22, 合计后舍入=0.21
1.0.equals(1.00)=false, compareTo=0
1/3不指定精度会报错=true, 指定2位结果=0.33

这里的HALF_UP是本例明确选择的舍入方式,不意味着所有价格、税额或分摊业务都应使用它。生产代码应从对应业务规则中确定方式与保留位数,测试预期也要来自同一规则。

compareTo与equals不能随手互换

1.0和1.00的数值相同,compareTo返回0;equals还比较scale,所以返回false。Map键和HashSet去重通常遵循equals与hashCode,业务若只认数值相等,需要明确统一表示方式或实现相应的比较逻辑,不能因为一次compareTo相等,就假设容器也会自动合并它们。

除法则检查另一类约定:1/3没有有限位十进制结果,直接调用不带精度或舍入参数的divide会抛ArithmeticException。代码又运行了明确保留两位的版本,得到0.33,但这只是指定计算规则后的结果,并没有实现分摊余数处理。

定位差一分的问题时,可以按原始输入、每行中间值、舍入结果、总计四项逐步比较。哪一步开始不同,就在那一步核对规则。先不要靠"最后统一加一分"把差异抹掉,那会让下一种输入继续出问题。

如果明细逐行显示0.11元,两行的总计却是0.21元,你们会先核对哪一步?业务要求逐行舍入还是合计后舍入,页面、导出和结算是否用的是同一条规则?

相关推荐
时光不老杰1 小时前
01 总体架构与模块边界
后端
贾公子1 小时前
从单条内容到账号维度——AIGC 风险评分的工程实现
后端·python
一条泥憨鱼1 小时前
【消息队列】ACK确认了什么:消息队列可靠投递的完整链路
java·后端·kafka·消息队列·rabbitmq
从琳开始9892 小时前
网络原理入门(二)------OSI七层模型?TCP/IP五层(四层)模型原来是这样的
java·网络·tcp/ip·java-ee·php
文丑颜不良啊2 小时前
LeetCode1861~1880题目解析
java·leetcode
Flynn Liang2 小时前
AgentScope Java 深度解析:ReActAgent、HarnessAgent 与多智能体编排——JVM 上的自治 Agent 底座
java·大模型·架构设计·ai agent·spring ai·langchain4j·agentscope
Flynn Liang2 小时前
上线后怎么排查 AI 调用?Spring AI Observability:token/延迟/成本埋点与全链路追踪
java·人工智能·spring·监控·可观测性·observability·springai
文丑颜不良啊3 小时前
LeetCode1921~1940题目解析
java·leetcode
孟陬3 小时前
Foobar 技术周刊#160:大模型让大规模形式化软件验证成为现实丨AI写C++代码之后会发生什么?丨我们真的需要alpha() 函数吗丨重试,有时会让故障更
前端·后端·typescript