BigDecimal 精度问题

面试官问:"为什么不能用 double 做金额计算?"

候选人小孟答:"因为 double 有精度损失。"

面试官追问:"0.1 + 0.2 等于多少?"

小孟说:"0.3?"

面试官:"你自己运行一下代码看看。"

小孟运行后发现是 0.30000000000000004。

小孟愣住了。

【面试官心理】 这道题看似简单,但能准确说出浮点数精度问题根源的候选人不多。追问 double 不能用于金额计算的原因,是测试候选人对数值类型的理解深度。

一、浮点数精度问题根源 🔴

1.1 为什么 0.1 + 0.2 ≠ 0.3

System.out.println(0.1 + 0.2); // 输出:0.30000000000000004
System.out.println(0.1 * 0.1); // 输出:0.010000000000000002

原因:0.1 在二进制中是无限循环小数,只能近似表示:

十进制 0.1 = 二进制 0.0001100110011001100110011...(无限循环)
二进制表示只能是截断的近似值

1.2 double 的 IEEE 754 标准

// double 占 64 位(8 字节)
// 1 位符号 + 11 位指数 + 52 位尾数

// 0.1 的实际存储值:
// 0.1000000000000000055511151231257827021181583404541015625

// 所以:
// 0.1 实际存储值 = 0.100000000000000005551115...
// 0.2 实际存储值 = 0.200000000000000011102...
// 相加后 = 0.300000000000000044408...

二、BigDecimal 基础 🔴

2.1 创建 BigDecimal 的陷阱

// ❌ 错误:用 double 创建 BigDecimal
BigDecimal bd1 = new BigDecimal(0.1); // bd1 = 0.1000000000000000055511151231257827021181583404541015625
BigDecimal bd2 = new BigDecimal(0.2); // bd2 = 0.200000000000000011102...
bd1.add(bd2); // 结果仍然是 0.30000000000000004

// ✅ 正确:用 String 创建
BigDecimal bd3 = new BigDecimal("0.1"); // bd3 = 0.1(精确值)
BigDecimal bd4 = new BigDecimal("0.2"); // bd4 = 0.2(精确值)
bd3.add(bd4); // 结果 = 0.3(精确值)

// ✅✅ 最佳:用 valueOf(推荐)
BigDecimal bd5 = BigDecimal.valueOf(0.1); // 内部会转为 String
BigDecimal bd6 = BigDecimal.valueOf(0.2);
bd5.add(bd6); // 结果 = 0.3(精确值)
⚠️

永远不要用 new BigDecimal(double) 创建金额计算用的 BigDecimal。一定要用 new BigDecimal(String)BigDecimal.valueOf(double)

2.2 BigDecimal 的内部表示

// BigDecimal = unscaled value × 10^(-scale)
// unscaled value: BigInteger 类型的整数
// scale: int 类型的缩放因子

// "0.1" 的内部表示:
// unscaledValue = 1
// scale = 1
// 实际值 = 1 × 10^(-1) = 0.1

// "0.100" 的内部表示:
// unscaledValue = 100
// scale = 3
// 实际值 = 100 × 10^(-3) = 0.100

// "123.45" 的内部表示:
// unscaledValue = 12345
// scale = 2
// 实际值 = 12345 × 10^(-2) = 123.45

三、金额计算的规范操作 🔴

3.1 加减乘除

BigDecimal amount1 = new BigDecimal("10.00");
BigDecimal amount2 = new BigDecimal("3.50");

// 加法:scale 取两个数中较大的
amount1.add(amount2); // 13.50

// 减法
amount1.subtract(amount2); // 6.50

// 乘法
amount1.multiply(amount2); // 35.0000(需要指定舍入模式)

// 除法(必须指定舍入模式)
amount1.divide(amount2, 2, RoundingMode.HALF_UP); // 2.86

3.2 设置精度和舍入模式

// 常用舍入模式
RoundingMode.UP      // 远离零舍入
RoundingMode.DOWN   // 接近零舍入
RoundingMode.CEILING // 接近正无穷舍入
RoundingMode.FLOOR  // 接近负无穷舍入
RoundingMode.HALF_UP   // 四舍五入
RoundingMode.HALF_DOWN // 五舍六入
RoundingMode.HALF_EVEN  // 银行家舍入(JDK 8+ 默认)

// 场景:
// 金额计算:通常用 HALF_UP(四舍五入)
// 国际化:可能需要 HALF_EVEN(银行家舍入)

3.3 金额计算的坑

// ❌ 错误:除法不指定舍入模式
BigDecimal bd = new BigDecimal("1").divide(new BigDecimal("3"));
// 抛出 ArithmeticException:Non-terminating decimal expansion

// ✅ 正确:指定舍入模式
BigDecimal bd = new BigDecimal("1")
    .divide(new BigDecimal("3"), 10, RoundingMode.HALF_UP);
// 结果:0.3333333333

四、equals vs compareTo 🟡

// ❌ 错误:用 equals 比较值
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
BigDecimal c = new BigDecimal("1.0");

a.equals(b); // false!因为 scale 不同(1 vs 2)
a.equals(c); // true

// ✅ 正确:用 compareTo 比较值
a.compareTo(b) == 0; // true!只比较数值
a.compareTo(c) == 0; // true

// 金额比较应该用 compareTo

五、生产金额计算工具类 🟡

public class Money {
    private final BigDecimal amount;
    private static final int SCALE = 2; // 精确到分

    public Money(long cents) {
        this.amount = BigDecimal.valueOf(cents, 2);
    }

    public Money add(Money other) {
        return new Money(this.amount.add(other.amount));
    }

    public Money subtract(Money other) {
        return new Money(this.amount.subtract(other.amount));
    }

    public Money multiply(double factor) {
        BigDecimal result = this.amount.multiply(
            BigDecimal.valueOf(factor));
        return new Money(result.setScale(SCALE, RoundingMode.HALF_UP));
    }

    public BigDecimal divide(Money divisor) {
        return this.amount.divide(divisor.amount, SCALE, RoundingMode.HALF_UP);
    }

    @Override
    public String toString() {
        return amount.setScale(SCALE, RoundingMode.HALF_UP).toString();
    }
}

六、追问升级

面试官:"BigDecimal 是线程安全的吗?"

// BigDecimal 是不可变对象(immutable)
// 所有字段都是 final 的
// 所有操作都返回新的 BigDecimal
// ✅ 线程安全,可以安全共享

【面试官心理】 能说出 BigDecimal 不可变性的候选人,说明理解并发编程的基本原则。这是 P6 的要求。