<span class=“js_title_inner“>别用 new BigDecimal(0.1)!这是 90% Java 程序员都会踩的坑</span>
2026/3/21 9:54:36 网站建设 项目流程
关注我们,设为星标,每天7:30不见不散,每日java干货分享


🧮 浮点数:理想中的“精密数学”

在人类的直觉里,小数运算是天经地义的:

动作

代码行数 (理想状态)

描述

第一笔钱

1 行

double a = 0.1;

第二笔钱

1 行

double b = 0.2;

相加

1 行

double sum = a + b;

判断

1 行

if (sum == 0.3) pay();

结果

-

支付成功,账目平齐。

现实是:支付失败。因为sum的值是0.30000000000000004
你的代码走进了一个平行宇宙,那里0.3 != 0.3


🧬 第一关:二进制的“翻译丢失” (IEEE 754)

这是所有浮点数问题的根源。
计算机是二进制的(只有 0 和 1)。
人类是十进制的(0-9)。

恐怖故事:

  • • 整数0.5(十进制) =0.1(二进制)。这个能除尽,没问题。

  • • 小数0.1(十进制) =0.00011001100110011...(二进制)。

  • 发现了吗?它是无限循环小数!

就像你没法用“十进制”精确表示1/3(0.3333...) 一样,计算机也没法用“二进制”精确表示0.1
它只能截断,存一个“近似值”。
当你把两个“近似值”相加,误差就被放大了。


💸 第二关:消失的几分钱 (Financial Disaster)

这是电商和金融系统最容易踩的雷。

场景:
你在做一个电商后台。商品价格是19.90元,用户买了3个。
你写了:double total = 19.90 * 3;

恐怖故事:

  • • 你的预期:59.70

  • • 计算机的结果:59.699999999999996

后果:

  • 前端展示:用户看到了59.6999...,觉得你们系统有 Bug。

  • 数据库存入:如果你截取两位小数存入,可能变成了59.69少了 1 分钱。

  • 财务审计:累计几亿笔订单后,账面上莫名其妙少了几百万

  • 结局:程序员被祭天,因为涉嫌“贪污”那消失的 1 分钱。


♾️ 第三关:死循环的陷阱 (The Infinite Loop)

场景:
你想写一个循环,从 0 开始,每次加 0.1,直到等于 1。

for (double i = 0; i != 1.0; i += 0.1) { System.out.println("Running..."); }

恐怖故事:
这个循环永远不会停止

真相:

  • i的值变化:

  • • 0

  • • 0.1

  • • 0.2

  • 0.30000000000000004(这就是鬼故事的开始)

  • • ...

  • • 最后它会变成0.999999...然后直接跳到1.099999...

  • • 它永远不会精确地等于1.0

后果:
服务器 CPU 100%,线程卡死。你需要重启服务才能救活它。


🚀 第四关:价值 3.7 亿美元的 Bug (Ariane 5)

这是历史上最昂贵的浮点数事故。

时间:1996 年 6 月 4 日。
事件:欧洲航天局的阿里亚纳 5 号火箭首飞。
结果:发射后 37 秒,火箭在空中解体爆炸。

代码真相:
程序试图把一个64 位浮点数(火箭的水平速度)转换成一个16 位有符号整数
当时火箭速度太快,浮点数的值超过了 16 位整数的最大范围(32767)。
结果:溢出报错 (Overflow)-> 导航计算机死机 -> 备份计算机也死机(跑的是同一套代码) -> 火箭启动自毁程序。
损失:3.7 亿美元瞬间化为乌有。


🧟‍♂️ 第五关:NaN 的僵尸病毒

浮点数里有一个特殊值叫NaN (Not a Number)
它比如0.0 / 0.0或者Math.sqrt(-1)会产生。

恐怖故事:
NaN 有一个极其反直觉的特性:NaN 不等于 NaN
if (x == x)在 x 是 NaN 时,结果是false

后果:
如果你在一个列表中混入了一个NaN,然后对列表进行排序
排序算法(如 Timsort)依赖x > yx == y的比较逻辑。
因为NaN跟谁比都是错,排序可能会崩溃,或者陷入死循环,或者排出来的顺序是乱的。
NaN就像僵尸病毒,一旦进入你的数据流,所有的后续计算都会变成NaN


🛡️ 拆弹专家:如何正确算账?

既然浮点数这么不靠谱,我们该怎么办?

1. 金融计算:严禁使用 Float/Double

涉及钱的地方,必须使用高精度小数类

  • Java:BigDecimal

  • Python:decimal.Decimal

  • SQL:DECIMAL(10, 2)

正确姿势:

// 千万别用 new BigDecimal(0.1),因为它会把 0.1 的误差也存进去! // 要用 String 构造器 BigDecimal a = new BigDecimal("0.1"); BigDecimal b = new BigDecimal("0.2"); BigDecimal sum = a.add(b); // 结果真的是 0.3
2. 卑微的妥协:Epsilon 比较法

如果你非要用浮点数(比如做游戏、图形学计算),**永远不要用==**
要判断两个数是否“足够接近”。

正确姿势:

double EPSILON = 0.000001; if (Math.abs(a - b) < EPSILON) { // 认为是相等的 }
3. 变成整数:单位降级

把“元”转换成“分”来存储。

  • • 存1990分,而不是19.9元。

  • • 所有的计算全用整数(整数是没有精度丢失的)。

  • • 只在显示给用户看的时候,除以 100。


💡 终章:计算机的“失语”

计算机并不完美。
它引以为傲的计算能力,建立在二进制的沙滩上。
当你想用这堆沙子去构建人类十进制的大厦时,必须小心翼翼地填补那些**“精度的缝隙”**。

推荐阅读 点击标题可跳转

50个Java代码示例:全面掌握Lambda表达式与Stream API

16 个 Java 代码“痛点”大改造:“一般写法” VS “高级写法”终极对决,看完代码质量飙升!

为什么高级 Java 开发工程师喜爱用策略模式

精选Java代码片段:覆盖10个常见编程场景的更优写法

提升Java代码可靠性:5个异常处理最佳实践

为什么大佬的代码中几乎看不到 if-else,因为他们都用这个...

还在 Service 里疯狂注入其他 Service?你早就该用 Spring 的事件机制了

看完本文有收获?请转发分享给更多人

关注「java干货」加星标,提升java技能

❤️给个「推荐 」,是最大的支持❤️

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询