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

发布时间:2026/7/27 10:26:42
Java 用 double 算钱算出 0.30000000000000004:BigDecimal 正确姿势与坑 Java 用 double 算钱算出 0.30000000000000004:BigDecimal 正确姿势与坑写电商、财务、账单系统时,几乎每个 Java 程序员都被这行代码坑过:System.out.println(0.10.2);// 0.30000000000000004明明是小学算术,结果多出一串诡异的小数。如果你拿double存过金额,迟早会在对账时收到「差了一分钱」的告警。这篇讲清楚为什么会这样、BigDecimal怎么用才对,以及它自己也藏着的几个坑。为什么 double 算不准钱double/float是 IEEE 754 二进制浮点数。计算机用二进制表示小数,而0.1、0.2这种十进制小数在二进制里是无限循环的,就像十进制里的1/3 0.333...永远写不完。存进 64 位double时只能截断,于是存的其实是一个非常接近但不等于0.1的值,累加起来误差就暴露了。// 误差在比较时最致命doubletotal0.10.2;if(total0.3){System.out.println(相等);}else{System.out.println(不相等);// 实际走这里}结论:只要涉及金额、利率、税费这类需要精确十进制的场景,永远不要用 double/float。用BigDecimal,或者用「以分为单位的 long」(下面会讲)。BigDecimal 的第一个坑:别用 double 构造很多人知道要用BigDecimal,但第一步就错了:BigDecimalanewBigDecimal(0.1);// 错!System.out.println(a);// 0.1000000000000000055511151231257827021181583404541015625new BigDecimal(double)接收的是那个已经不精确的 double,BigDecimal 只是忠实地把这个二进制误差原样展开,反而更难看。正确做法是用字符串构造,或者用valueOf:BigDecimalanewBigDecimal(0.1);// 对:字符串精确BigDecimalbBigDecimal.valueOf(0.1);// 对:valueOf 内部走 Double.toString,结果是 0.1System.out.println(a.add(newBigDecimal(0.2)));// 0.3记住规则:构造 BigDecimal 用 String,别用 double。BigDecimal.valueOf(double)也安全,因为它内部先把 double 转成规整的字符串。第二个坑:除法不指定精度会抛异常BigDecimalanewBigDecimal(1);BigDecimalbnewBigDecimal(3);System.out.println(a.divide(b));// 抛 ArithmeticException: Non-terminating decimal expansion1/3是无限小数,BigDecimal 不知道你要保留几位,直接摆烂抛异常。除法必须指定保留位数和舍入模式:// 保留 2 位小数,四舍五入(HALF_UP)BigDecimalresulta.divide(b,2,RoundingMode.HALF_UP);System.out.println(result);// 0.33舍入模式常用这几个:HALF_UP:四舍五入(日常最常用)。HALF_EVEN:银行家舍入,0.5时向偶数靠,长期统计更无偏,金融系统常用。DOWN:直接截断(算利息时对用户不利,慎用)。第三个坑:equals 比较值相等会翻车BigDecimal的equals会连精度(scale)一起比,这经常不是你想要的:BigDecimalxnewBigDecimal(1.0);BigDecimalynewBigDecimal(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 是不可变的BigDecimal和String一样不可变,所有运算都返回新对象,不改原值:BigDecimalpricenewBigDecimal(100);price.add(newBigDecimal(50));// 返回值被丢弃了!price 还是 100System.out.println(price);// 100priceprice.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){BigDecimalpricenewBigDecimal(19.99);BigDecimalqtynewBigDecimal(3);BigDecimaldiscountnewBigDecimal(0.85);BigDecimaltotalprice.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存分(或更小单位),彻底绕开浮点:longpriceCents1999;// 19.99 元longtotalpriceCents*3;// 5997 分,整数运算精确又快// 展示时再 total / 100 . total % 100long存分的优点是快、无精度问题、比较简单;缺点是遇到「按比例分摊」「多币种、多位小数」时不如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——这四条守住,对账再也不差一分钱。