WMX · 随记
·Java· 5 分钟阅读

金额计算里的 BigDecimal:精度与舍入要分开想

为什么从字符串创建十进制数,以及 equals、compareTo 和舍入规则各自负责什么。

十进制计算示意:从文本解析数值,使用 BigDecimal 运算,在业务边界指定精度与舍入。
十进制计算示意:从文本解析数值,使用 BigDecimal 运算,在业务边界指定精度与舍入。 点击图片查看大图 ↗

金额问题里,“显示两位小数”和“计算正确”是两件不同的事。前者是输出规则,后者涉及输入表示、运算顺序和舍入时机。BigDecimal 提供十进制运算能力,但业务规则仍然要自己定义。

先别让输入绕一圈二进制浮点数

Java22 行 · 可横向滚动
import java.math.BigDecimal;
import java.math.RoundingMode;

public class MoneyDemo {
    public static void main(String[] args) {
        BigDecimal price = new BigDecimal("19.90");
        BigDecimal quantity = new BigDecimal("3");
        BigDecimal discount = new BigDecimal("0.85");

        BigDecimal subtotal = price.multiply(quantity);
        BigDecimal payable = subtotal.multiply(discount)
            .setScale(2, RoundingMode.HALF_UP);
        System.out.println(payable.toPlainString()); // 50.75

        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1.00");
        System.out.println(a.equals(b));           // false
        System.out.println(a.compareTo(b) == 0);   // true
        System.out.println(new BigDecimal("1")
            .divide(new BigDecimal("3"), 4, RoundingMode.HALF_UP));
    }
}

通过字符串构造,可以直接表达十进制输入。new BigDecimal(0.1) 会准确保留传入 double 的二进制近似值,而不是你眼中的十进制 0.1。已有 double 输入时,BigDecimal.valueOf 使用其标准字符串表示,通常比直接构造更符合直觉,但它不能恢复上游已经损失的精度。

数值相等,表示不一定相等

equals 同时考虑数值和 scale,因此 1.0 与 1.00 不相等;compareTo 做数值比较时会把它们视作相等。这个区别在 HashMap 键、去重和测试断言里很容易被忽略。系统需要先统一规则,而不是在不同地方随机选择比较方式。

舍入应该发生在哪一层

单行商品各自舍入再汇总,与先汇总再舍入,可能产生不同结果。税额、折扣和分摊还会带来更多规则。把“每一步都保留两位”当作通用答案,会把表示层的习惯混进计算层。

  • 确认币种:并不是所有币种都以两位小数为常用最小单位。
  • 定义舍入模式:HALF_UP 只是一个选项,不能代替财务规则。
  • 处理不能有限表示的除法:显式给出 scale 和 roundingMode,或使用合适的 MathContext。
  • 为分摊定义余数归属:几个人分一笔钱时,总额仍然需要守恒。

给规则写小而具体的例子

测试应该围绕业务边界:0.005 如何舍入、退款是否允许负数、折扣顺序是否固定、分摊后合计是否等于原金额。让规则能够被复核,比仅仅把 double 全部替换成 BigDecimal 更有价值。

继续阅读

← 返回文章归档