关注我们,设为星标,每天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 > y或x == 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.32. 卑微的妥协: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;}