DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Java BigDecimal 已设置小数位,divide 仍报 Non-terminating decimal expansion

BigDecimal 精确除法可能在 setScale 执行前就因无限循环小数失败。直接指定除法的 scale 和舍入规则,并区分小数位、有效数字及被除数 scale。

给两个输入都设置了两位小数,除法后也调用 setScale,仍在 divide 处抛出 ArithmeticException:

BigDecimal numerator = new BigDecimal("1131.00");
BigDecimal denominator = new BigDecimal("365.00");
BigDecimal result = numerator.divide(denominator)
    .setScale(2, RoundingMode.HALF_UP);

报错通常是 Non-terminating decimal expansion; no exact representable decimal result。这组数相除不能得到有限位的精确十进制结果,而不带舍入参数的 divide 要求精确表示。后面的 setScale 还没有机会执行。

本文使用标准库 BigDecimal、MathContext 和 RoundingMode,涉及的除法重载从 Java 5 起提供。示例中的两位小数与舍入模式仅演示接口选择,实际精度须遵守业务规则。

在发生除法时指定结果的小数位

如果需求是最终保留两位小数,把 scale 与舍入模式直接传给除法:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class DivideExample {
    public static void main(String[] args) {
        BigDecimal numerator = new BigDecimal("1131.00");
        BigDecimal denominator = new BigDecimal("365.00");
        BigDecimal result = numerator.divide(
            denominator, 2, RoundingMode.HALF_UP
        );
        System.out.println(result.toPlainString());
    }
}

按这组参数,结果预期为 3.10。舍入发生在生成商的过程中,因此不需要先构造无限位的中间值。只给输入设置 scale,并不会把“结果也保留两位”的要求传递给单参数除法。

BigDecimal 不可变,setScale 返回调整后的值,不能忽略返回值并期待原对象被修改。即使接住返回值,发生在它之前的精确除法异常仍需单独处理。

两位有效数字与两位小数不同

MathContext 的 precision 指有效数字位数,不是小数点后的位数:

BigDecimal a = new BigDecimal("1131");
BigDecimal b = new BigDecimal("365");

BigDecimal byScale = a.divide(b, 2, RoundingMode.HALF_UP);
BigDecimal byPrecision = a.divide(
    b, new java.math.MathContext(2, RoundingMode.HALF_UP)
);
// byScale 预期为 3.10
// byPrecision 预期为 3.1

当整数部分变长时,两种规则的差别会更明显。选择 MathContext 应是因为算法要求控制有效数字,而不是把构造器中的 2 当作两位小数。

还有一个容易忽略的重载:a.divide(b, RoundingMode.HALF_UP) 使用被除数 a 的 scale。这里 a 由 "1131" 构造,scale 为零,该重载就会按零位小数生成结果。如果结果格式是接口约定,直接写出 scale 更便于核对。

仍报错时区分三种条件

指定了舍入仍不能解决除数为零;应按业务规则拒绝零除数,而不是返回一个看似正常的值。RoundingMode.UNNECESSARY 表示断言不需要舍入,实际需要舍入时仍会抛异常。MathContext 的 precision 为零则要求精确运算,对无限循环小数仍可能失败。

另外,舍入方式会影响数值,不能把所有异常都用任意一种舍入模式压掉。HALF_UP 在恰好中点时向远离零的方向舍入,负数也要纳入预期结果;如果协议规定另一种规则,应采用相应模式。

验证时至少覆盖可精确除尽、循环小数、负数和零除数,并分别检查数值与输出 scale。不要把生产计算整体替换后只确认“没有异常”:精度变化可能影响后续累计或比较。

保留原输入与原有精度规则便于回退。已经舍入的结果通常无法恢复原始精度,需要用原输入重新计算;改回代码不会自动纠正已经保存或发送的结果。