Java 用 double 算钱算出 0.30000000000000004:BigDecimal 正确姿势与坑

📅 2026/7/27 10:30:24 👁️ 阅读次数 📝 编程学习
Java 用 double 算钱算出 0.30000000000000004:BigDecimal 正确姿势与坑

Java 用 double 算钱算出 0.30000000000000004:BigDecimal 正确姿势与坑

写电商、财务、账单系统时,几乎每个 Java 程序员都被这行代码坑过:

System.out.println(0.1+0.2);// 0.30000000000000004

明明是小学算术,结果多出一串诡异的小数。如果你拿double存过金额,迟早会在对账时收到「差了一分钱」的告警。这篇讲清楚为什么会这样、BigDecimal怎么用才对,以及它自己也藏着的几个坑。

为什么 double 算不准钱

double/float是 IEEE 754 二进制浮点数。计算机用二进制表示小数,而0.10.2这种十进制小数在二进制里是无限循环的,就像十进制里的1/3 = 0.333...永远写不完。存进 64 位double时只能截断,于是存的其实是一个非常接近但不等于0.1的值,累加起来误差就暴露了。

// 误差在比较时最致命doubletotal=0.1+0.2;if(total==0.3){System.out.println("相等");}else{System.out.println("不相等");// 实际走这里}

结论:只要涉及金额、利率、税费这类需要精确十进制的场景,永远不要用 double/float。BigDecimal,或者用「以分为单位的 long」(下面会讲)。

BigDecimal 的第一个坑:别用 double 构造

很多人知道要用BigDecimal,但第一步就错了:

BigDecimala=newBigDecimal(0.1);// 错!System.out.println(a);// 0.1000000000000000055511151231257827021181583404541015625

new BigDecimal(double)接收的是那个已经不精确的 double,BigDecimal 只是忠实地把这个二进制误差原样展开,反而更难看。

正确做法是用字符串构造,或者用valueOf:

BigDecimala=newBigDecimal("0.1");// 对:字符串精确BigDecimalb=BigDecimal.valueOf(0.1);// 对:valueOf 内部走 Double.toString,结果是 "0.1"System.out.println(a.add(newBigDecimal("0.2")));// 0.3

记住规则:构造 BigDecimal 用 String,别用 double。BigDecimal.valueOf(double)也安全,因为它内部先把 double 转成规整的字符串。

第二个坑:除法不指定精度会抛异常

BigDecimala=newBigDecimal("1");BigDecimalb=newBigDecimal("3");System.out.println(a.divide(b));// 抛 ArithmeticException: Non-terminating decimal expansion

1/3是无限小数,BigDecimal 不知道你要保留几位,直接摆烂抛异常。除法必须指定保留位数和舍入模式:

// 保留 2 位小数,四舍五入(HALF_UP)BigDecimalresult=a.divide(b,2,RoundingMode.HALF_UP);System.out.println(result);// 0.33

舍入模式常用这几个:

  • HALF_UP:四舍五入(日常最常用)。
  • HALF_EVEN:银行家舍入,0.5时向偶数靠,长期统计更无偏,金融系统常用。
  • DOWN:直接截断(算利息时对用户不利,慎用)。

第三个坑:equals 比较值相等会翻车

BigDecimalequals连精度(scale)一起比,这经常不是你想要的:

BigDecimalx=newBigDecimal("1.0");BigDecimaly=newBigDecimal("1.00");System.out.println(x.equals(y));// false!scale 不同(1 位 vs 2 位)System.out.println(x.compareTo(y)==0);// true:只比数值

要判断「数值是否相等」,一律用compareTo(...) == 0,不要用equals。这个坑在写单元测试断言时尤其容易踩。

第四个坑:BigDecimal 是不可变的

BigDecimalString一样不可变,所有运算都返回新对象,不改原值:

BigDecimalprice=newBigDecimal("100");price.add(newBigDecimal("50"));// 返回值被丢弃了!price 还是 100System.out.println(price);// 100price=price.add(newBigDecimal("50"));// 对:接住返回值System.out.println(price);// 150

漏接返回值是新手高频 bug,写的时候留个心眼:BigDecimal 的每个运算都要x = x.op(...)

一个完整的金额计算示例

算一个订单:单价 19.99,数量 3,打 8.5 折,保留 2 位:

importjava.math.BigDecimal;importjava.math.RoundingMode;publicclassOrderCalc{publicstaticvoidmain(String[]args){BigDecimalprice=newBigDecimal("19.99");BigDecimalqty=newBigDecimal("3");BigDecimaldiscount=newBigDecimal("0.85");BigDecimaltotal=price.multiply(qty)// 59.97.multiply(discount)// 50.9745.setScale(2,RoundingMode.HALF_UP);// 50.97System.out.println("应付:"+total);// 应付:50.97}}

setScale(2, RoundingMode.HALF_UP)是收尾定妆的关键:把中间过程的多余小数按业务规则规整到「分」。中间计算可以保留高精度,最后一步再 setScale,不要每一步都截断,否则误差会累积。

什么时候用 long 存分

如果金额永远是「元 + 分」两位精度、不涉及复杂的除法分摊,很多高性能系统干脆用long(或更小单位),彻底绕开浮点:

longpriceCents=1999;// 19.99 元longtotal=priceCents*3;// 5997 分,整数运算精确又快// 展示时再 total / 100 + "." + total % 100

long存分的优点是快、无精度问题、比较简单;缺点是遇到「按比例分摊」「多币种、多位小数」时不如BigDecimal灵活。经验法则:简单收付款用 long 存分,涉及税率、利率、分摊、汇率的用 BigDecimal。

小结

  • double/float是二进制浮点,存不下0.1这类十进制小数,算钱一律禁用
  • 构造BigDecimal用字符串(new BigDecimal("0.1"))或valueOf,绝不用new BigDecimal(0.1)
  • 除法必须给精度和舍入模式(divide(b, 2, RoundingMode.HALF_UP)),否则无限小数直接抛异常。
  • 比较数值用compareTo() == 0,别用equals(它连 scale 一起比)。
  • BigDecimal不可变,每步运算都要x = x.op(...)接住返回值。
  • 精度固定的简单收付款可以用long存分,更快更省心。

一句话记忆:算钱,构造用 String、除法给精度、比较用 compareTo、收尾用 setScale——这四条守住,对账再也不差一分钱。