本文目录导读:

- 核心原则:用对类
- 避免
BigDecimal构造误差 - 除法运算:指定舍入模式
- 比较相等:用
compareTo而非equals - 金融计算典型案例:避免 double 累积误差
- 避免
BigInteger的常见陷阱 - 性能与精度权衡(合理缓存)
- 完整案例:电商订单金额计算
- 总结:避免误差 checklist
在Java中处理大数运算时,避免误差的关键在于选择正确的数据类型,主要涉及 BigInteger(整数)和 BigDecimal(浮点数)两个类。
以下分场景说明如何避免误差,并附带典型案例。
核心原则:用对类
| 业务场景 | 推荐类 | 原因 |
|---|---|---|
| 精确整数(金额分、ID、密码学) | BigInteger |
任意精度,无上限 |
| 精确小数(金额元、科学计算) | BigDecimal |
避免 float/double 的二进制浮点误差 |
| 高性能、允许误差 | double / float |
不推荐用于金融、计费 |
避免 BigDecimal 构造误差
错误写法:
BigDecimal a = new BigDecimal(0.1); // 打印出来是 0.100000000000000005551115... BigDecimal b = new BigDecimal(0.2); BigDecimal sum = a.add(b); // 结果是 0.300000000000000044408920985006...
正确写法:
BigDecimal a = new BigDecimal("0.1"); // 使用字符串构造
BigDecimal b = new BigDecimal("0.2");
BigDecimal sum = a.add(b); // 结果是 0.3(精确)
// 或者使用静态方法 valueOf(推荐)
BigDecimal c = BigDecimal.valueOf(0.1);
BigDecimal d = BigDecimal.valueOf(0.2);
原理:
new BigDecimal(double) 会尝试用二进制精确表示 1,但 1 在二进制中是无限循环小数,导致构造时就已经产生了误差。字符串构造则直接解析十进制字符串,无中间转换误差。
除法运算:指定舍入模式
BigDecimal 的除法必须指定精度(scale) 和舍入模式,否则对于无限小数会抛出 ArithmeticException。
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
// 错误:会抛出 ArithmeticException
// BigDecimal result = a.divide(b);
// 正确:指定精度和舍入模式
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);
// 结果:3.33
常用舍入模式:
HALF_UP:四舍五入(银行家算法替代方案,金融常用)HALF_EVEN:银行家舍入法(四舍六入五成双,统计学最优)DOWN:直接截断(不向上进位)UP:远离零舍入(全部进位)
比较相等:用 compareTo 而非 equals
BigDecimal a = new BigDecimal("1.00");
BigDecimal b = new BigDecimal("1.0");
a.equals(b); // false(因为 scale 不同:2 vs 1)
a.compareTo(b) == 0; // true(忽略小数位数,只比较数值)
错误后果:
使用 equals 比较时,00 和 0 被视为不相等,可能导致校验失败。
金融计算典型案例:避免 double 累积误差
场景: 计算利息,日复利,每天增加 0.001 倍。
// ❌ 错误:使用 double
double amount = 100.0;
double rate = 0.001;
for (int i = 0; i < 365; i++) {
amount = amount * (1 + rate); // 累积误差越来越大
}
// ✅ 正确:使用 BigDecimal
BigDecimal amount = new BigDecimal("100");
BigDecimal rate = new BigDecimal("0.001");
BigDecimal one = BigDecimal.ONE;
for (int i = 0; i < 365; i++) {
amount = amount.multiply(one.add(rate));
}
避免 BigInteger 的常见陷阱
整数除法丢失精度:
BigInteger a = new BigInteger("10");
BigInteger b = new BigInteger("3");
// 正确:需要保留小数时,应使用 BigDecimal
BigDecimal result = new BigDecimal(a).divide(new BigDecimal(b), 10, RoundingMode.HALF_UP);
不必要的内存爆炸:
BigInteger 没有上限,但极端大数的运算(如 pow(1000000))会耗尽内存或极慢,应提前预估数据范围。
性能与精度权衡(合理缓存)
// 不要重复创建常量对象
BigDecimal zero = BigDecimal.ZERO;
BigDecimal one = BigDecimal.ONE;
BigDecimal ten = BigDecimal.TEN;
// 而不是
// BigDecimal zero = new BigDecimal("0"); // 每次创建新对象
完整案例:电商订单金额计算
public class OrderCalculator {
public static void main(String[] args) {
// 商品单价(元)
BigDecimal price = new BigDecimal("19.99");
// 购买数量
BigDecimal quantity = new BigDecimal("3");
// 折扣率 10%(0.9)
BigDecimal discountRate = new BigDecimal("0.9");
// 运费
BigDecimal shipping = new BigDecimal("5.00");
// 计算总价
BigDecimal subtotal = price.multiply(quantity);
BigDecimal discounted = subtotal.multiply(discountRate)
.setScale(2, RoundingMode.HALF_UP); // 保留2位小数
BigDecimal total = discounted.add(shipping);
System.out.println("总金额: " + total); // 输出:58.97
}
}
避免误差 checklist
| 要点 | 做法 |
|---|---|
构造 BigDecimal |
必须用字符串 "0.1",或用 valueOf(double) |
| 除法 | 必须指定 scale 和 RoundingMode |
| 比较 | 用 compareTo() 而不是 equals() |
| 浮点数运算 | 坚决不用 double 做金融计算 |
| 性能 | 缓存 ZERO/ONE/TEN,避免循环创建 |
| 溢出预防 | 大数运算前预估数据范围,避免内存耗尽 |
一句话:只要涉及金额、计费、精确科学计算,一律用 BigDecimal(字符串构造)并指定舍入模式。