Java BigDecimal 全面解析

339 阅读2分钟

Java BigDecimal 全面解析

一、BigDecimal 用法

BigDecimal 用于高精度计算(如金融、科学计算),避免浮点数精度丢失问题。

1. 构造方法

BigDecimal a = new BigDecimal("0.1"); // 推荐:通过字符串构造
BigDecimal b = BigDecimal.valueOf(0.1); // 内部转字符串,安全
BigDecimal c = new BigDecimal(0.1); // 危险:直接使用 double 会有误差

2. 常用方法

a.add(b);      // 加法
a.subtract(b); // 减法
a.multiply(b); // 乘法
a.divide(b, 2, RoundingMode.HALF_UP); // 除法(必须指定精度和舍入模式)
a.setScale(2, RoundingMode.HALF_UP); // 设置精度和舍入
a.compareTo(b); // 比较大小(返回 -1/0/1)

二、实现原理

  • 底层结构:基于 BigInteger 的非压缩整数(intCompact)和标度(scale)表示数值。例如 123.45 表示为未缩放值 12345 和标度 2
  • 不可变性:所有操作返回新对象,线程安全但可能产生临时对象。
  • 性能:运算比 double 慢 10-100 倍,需权衡精度与效率。

三、对比其他数值类型

特性BigDecimaldouble/floatInteger/Long
精度精确(任意精度)近似(二进制浮点)精确(整数)
适用场景金融、科学计算一般计算整数运算
内存占用
运算速度

四、避坑指南

  1. 构造陷阱
    new BigDecimal(0.1); // 错误!实际值为 0.10000000000000000555...
    new BigDecimal("0.1"); // 正确
    
  2. 除法必须指定舍入
    a.divide(b); // 错误!可能抛 ArithmeticException
    a.divide(b, RoundingMode.HALF_UP); // 正确
    
  3. 等值比较
    a.equals(b); // 错误!比较值和标度(1.0 ≠ 1)
    a.compareTo(b) == 0; // 正确
    
  4. 精度丢失
    // 错误!未指定舍入模式可能导致精度丢失
    new BigDecimal("2.5").setScale(0); // 抛异常
    new BigDecimal("2.5").setScale(0, RoundingMode.HALF_UP); // 正确(结果为 3)
    

五、应用场景

  • 金融计算:货币金额、税率计算(精确到分)。
  • 科学计算:需要高精度小数的场景(如物理模拟)。
  • 数据统计:避免累积误差(如大规模数据汇总)。

六、实现四舍五入除法

使用 RoundingMode.HALF_UP 并指定精度:

BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33

七、保持百分比加和为 1

方法 1:余数补偿法

List<BigDecimal> percentages = new ArrayList<>();
BigDecimal total = BigDecimal.ZERO;
// 假设原始值已计算为 0.3333, 0.3333, 0.3334
for (int i = 0; i < percentages.size() - 1; i++) {
    percentages.set(i, percentages.get(i).setScale(2, RoundingMode.HALF_UP)); // 33.33%
    total = total.add(percentages.get(i));
}
// 最后一个元素补偿余数
BigDecimal last = BigDecimal.ONE.subtract(total);
percentages.set(percentages.size() - 1, last); // 0.3334 → 0.34

方法 2:高精度计算后调整

List<BigDecimal> values = Arrays.asList(new BigDecimal("30"), new BigDecimal("30"), new BigDecimal("40"));
BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
List<BigDecimal> percentages = values.stream()
    .map(v -> v.divide(sum, 4, RoundingMode.HALF_UP)) // 保留4位小数
    .map(v -> v.setScale(2, RoundingMode.HALF_UP))    // 四舍五入到2位
    .collect(Collectors.toList());
// 手动检查总和是否为 1,否则调整最后一个值

八、总结

  • 优点:精确计算、避免浮点误差。
  • 缺点:性能开销、代码冗余。
  • 最佳实践:始终通过字符串构造、明确指定精度和舍入模式、优先使用 compareTo 比较。