金额计算里的 BigDecimal:精度与舍入要分开想
为什么从字符串创建十进制数,以及 equals、compareTo 和舍入规则各自负责什么。
金额问题里,“显示两位小数”和“计算正确”是两件不同的事。前者是输出规则,后者涉及输入表示、运算顺序和舍入时机。BigDecimal 提供十进制运算能力,但业务规则仍然要自己定义。
先别让输入绕一圈二进制浮点数
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 更有价值。