1. BigDecimal的构造陷阱与精度问题
第一次用BigDecimal时,我踩了个大坑。当时需要计算商品价格,顺手写了new BigDecimal(0.1),结果打印出来的值让我傻眼了:0.1000000000000000055511151231257827021181583404541015625。后来才知道,用double类型构造BigDecimal就像用有漏洞的水桶装水,还没开始计算就已经漏了精度。
1.1 为什么double构造器会丢失精度
计算机用二进制存储double类型数据,而0.1在二进制中是无限循环小数(就像1/3在十进制中等于0.333...)。这导致double实际存储的值与表面值存在微小差异。测试代码很能说明问题:
System.out.println(new BigDecimal(0.1)); // 输出:0.100000000000000005551115123125... System.out.println(new BigDecimal("0.1")); // 输出:0.11.2 四种构造方式对比
| 构造方式 | 精度保证 | 推荐场景 | 性能开销 |
|---|---|---|---|
| BigDecimal(double) | ❌ | 不推荐使用 | 低 |
| BigDecimal(String) | ✅ | 金额字符串输入 | 中 |
| BigDecimal(int) | ✅ | 整数精确计算 | 低 |
| valueOf(double) | ✅ | 处理第三方double数据 | 中 |
最佳实践:财务系统里我坚持用字符串构造器,比如new BigDecimal("1250.99")。如果必须处理double数据(比如从数据库读取),就用BigDecimal.valueOf(0.1),它底层会先转字符串再构造。
2. 不可变性与四则运算的坑
去年团队有个新人写了这样的代码:
BigDecimal total = BigDecimal.ZERO; item.getPrice().add(discount); // 计算结果没保存结果订单总价永远为0,这就是没理解BigDecimal的不可变性(immutable)。所有运算都会返回新对象,原对象不变。
2.1 正确运算姿势
// 加法 BigDecimal a = new BigDecimal("1.23"); BigDecimal b = new BigDecimal("2.34"); BigDecimal sum = a.add(b); // 必须接收返回值 // 链式运算 BigDecimal result = a.add(b) .multiply(new BigDecimal("0.9")) .subtract(new BigDecimal("1.5"));2.2 财务计算三原则
- 始终用新对象接收结果:就像String的
+运算 - 检查Null:金额字段要判空
Objects.requireNonNull(amount) - 使用常量:
BigDecimal.ZERO比new BigDecimal("0")更高效
3. 除法运算的异常处理
做分账系统时,我遇到过最头疼的异常:
java.lang.ArithmeticException: Non-terminating decimal expansion当除不尽时(比如1除以3),必须指定舍入方式。
3.1 两种处理方案
方案一:指定精度和舍入模式
BigDecimal dividend = new BigDecimal("10"); BigDecimal divisor = new BigDecimal("3"); // 保留2位小数,四舍五入 BigDecimal quotient = dividend.divide(divisor, 2, RoundingMode.HALF_UP);方案二:使用MathContext
MathContext mc = new MathContext(4, RoundingMode.HALF_EVEN); // 银行家舍入法 BigDecimal result = dividend.divide(divisor, mc);3.2 财务舍入规则选择
| 舍入模式 | 1.255 | -1.255 | 适用场景 |
|---|---|---|---|
| HALF_UP(四舍五入) | 1.26 | -1.26 | 普通金额计算 |
| HALF_EVEN(银行家舍入) | 1.26 | -1.26 | 金融统计(减少偏差) |
| DOWN(截断) | 1.25 | -1.25 | 税费计算(利于纳税人) |
4. 金额比较与等值判断
早期我犯过用equals比较金额的错误:
BigDecimal d1 = new BigDecimal("2.0"); BigDecimal d2 = new BigDecimal("2.00"); System.out.println(d1.equals(d2)); // false!4.1 正确比较方式
值比较用compareTo:
if (d1.compareTo(d2) == 0) { // 数值相等(忽略精度) }严格比较用精度标准化:
// 统一保留2位小数再比较 boolean equal = d1.setScale(2, RoundingMode.HALF_UP) .equals(d2.setScale(2, RoundingMode.HALF_UP));5. 财务计算实战案例
最近做的电商优惠分摊系统,需要处理这样的场景:
- 订单总价:119.99元
- 优惠券减免:20元
- 需要按商品金额比例分摊优惠
5.1 分摊算法实现
List<Item> items = Arrays.asList( new Item("手机", new BigDecimal("5999.99")), new Item("耳机", new BigDecimal("399.99")) ); BigDecimal total = items.stream() .map(Item::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal coupon = new BigDecimal("20"); // 优惠券20元 items.forEach(item -> { // 计算分摊比例(保留6位小数) BigDecimal ratio = item.getPrice() .divide(total, 6, RoundingMode.HALF_UP); // 计算分摊金额(保留2位) BigDecimal discount = coupon.multiply(ratio) .setScale(2, RoundingMode.HALF_UP); System.out.printf("%s 分摊优惠:%.2f元%n", item.getName(), discount); });5.2 金额格式化的坑
直接调用toString()可能输出科学计数法:
System.out.println(new BigDecimal("123456789.12345")); // 输出:123456789.12345 System.out.println(new BigDecimal("123456789123.12345")); // 输出:1.2345678912312345E+11解决方案:
NumberFormat format = NumberFormat.getInstance(); format.setMinimumFractionDigits(2); format.setMaximumFractionDigits(2); format.setGroupingUsed(false); // 禁用千分位分隔符 System.out.println(format.format(new BigDecimal("123456789123.1"))); // 输出:123456789123.106. 性能优化技巧
在交易风控系统中,我们发现BigDecimal的这三个优化点最有效:
对象复用:对高频使用的值(如0、1、10)使用静态常量
private static final BigDecimal HUNDRED = new BigDecimal("100");控制精度:非必要不保留过多小数位
// 仅在做除法时指定精度 BigDecimal rate = amount.divide(total, 4, RoundingMode.HALF_UP);避免链式构造:
// 反例:产生中间对象 BigDecimal bad = new BigDecimal(new BigDecimal("123").add(new BigDecimal("456")).toString()); // 正例 BigDecimal good = new BigDecimal("123").add(new BigDecimal("456"));
实测下来,百万次运算能节省约200ms。虽然单次差异微小,但在高频交易场景下很关键。