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 倍,需权衡精度与效率。
三、对比其他数值类型
| 特性 | BigDecimal | double/float | Integer/Long |
|---|---|---|---|
| 精度 | 精确(任意精度) | 近似(二进制浮点) | 精确(整数) |
| 适用场景 | 金融、科学计算 | 一般计算 | 整数运算 |
| 内存占用 | 高 | 低 | 低 |
| 运算速度 | 慢 | 快 | 快 |
四、避坑指南
- 构造陷阱:
new BigDecimal(0.1); // 错误!实际值为 0.10000000000000000555... new BigDecimal("0.1"); // 正确 - 除法必须指定舍入:
a.divide(b); // 错误!可能抛 ArithmeticException a.divide(b, RoundingMode.HALF_UP); // 正确 - 等值比较:
a.equals(b); // 错误!比较值和标度(1.0 ≠ 1) a.compareTo(b) == 0; // 正确 - 精度丢失:
// 错误!未指定舍入模式可能导致精度丢失 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比较。