为什么 0.1 + 0.2 = 0.300000004
JavaScript 作为一门诞生自上个世纪 90 年代的编程语言[^1],从诞生之初就因为诡异的隐式类型转换等原因被黑,很多 JavaScript 的开发者还会吐槽浮点数加法的『奇葩』问题 — 为什么 0.1 + 0.2 在 JavaScript 中不等于 0.3,相信很多人都对这个问题的答案有一个大概的认识,但是都没有深入研究过,这个问题的答案让 William Kahan 在 1989 年获得图灵奖[^2]。
其实有上述问题的不止 JavaScript 一门编程语言,几乎所有现代的编程语言都会遇到上述问题,包括 Java、Ruby、Python、Swift 和 Go 等等,你可以在 https://0.30000000000000004.com/ 中找到常见的编程语言在计算上述表达式的结果[^3]。这不是因为它们在计算时出现了错误,而是因为浮点数计算标准的要求。> 0.1 + 0.2
0.30000000000000004

float
,然而在很长一段时间中,作者都将编程中的浮点数和数学中的小数看做同一个东西,不过当我们重新审视它们时,会发现这两个概念的不同之处。- 编程中的浮点数的精度往往都是有限的,单精度的浮点数使用 32 位表示,而双精度的浮点数使用 64 位表示;
- 数学中的小数系统可以通过引入无限序列
...
可以任意的实数[^4];
- 二进制无法在有限的长度中精确地表示十进制中 0.1 和 0.2;
- 单精度浮点数、双精度浮点数的位数决定了它们能够表示的精度上限;
二进制与十进制
我们日常生活中使用的数字基本都是 10 进制的,然而计算机使用二进制的 0 和 1 表示整数和小数,所有有限的十进制整数都可以无损的转换成有限长度的二进制数字,但是要在二进制的计算机中表示十进制的小数相对就很麻烦了,我们以 0.375 为例介绍它在二进制下的表示[^6]:[^6]: Decimal to Binary converter https://www.rapidtables.com/convert/number/decimal-to-binary.html小数点后面的位数依次表示十进制中的 0.5、0.25、0.125 和 0.0625 等等,这个表示方法非常好理解,每一位都是前一位的一半。0.375 在二进制表示看来确实是『整数』。然而如下图所示,想要使用二进制表示十进制中的 0.1 和 0.2 是比较复杂的:

精度上限
编程语言中的浮点数一般都是 32 位的单精度浮点数float
和 64 位的双精度浮点数 double
,部分语言会使用 float32
或者 float64
区分这两种不同精度的浮点数。想要使用有限的位数表示全部的实数是不可能的,不用说无限长度的小数和无理数,因为长度的限制,有限小数在浮点数中都无法精确的表示。
- 单精度浮点数
float
总共包含 32 位,其中 1 位表示符号、8 位表示指数,最后 23 位表示小数; - 双精度浮点数
double
总共包含 64 位,其中 1 位表示符号,11 位表示指数,最后 52 位表示小数;
[0, 126]
表示 [-127, -1]
,而 [127, 255]
表示 [0, 128]
,二进制的 01111100 是十进制的 124,表示 ,最后的 23 位是二进制的小数 0.25:

因为 0.1 和 0.2 使用单精度浮点数表示的实际值为 0.100000001490116119384765625 和 0.20000000298023223876953125[^7],所以它们在相加后就得到的结果与我们在一开始看到的非常相似:

总结
当我们在不同编程语言中看到 0.300000004 或者 0.30000000000000004 时不应该感到惊讶,这其实说明编程语言正确实现了 IEEE 754 标准中描述的浮点数系统,在使用单精度和双精度浮点数时也应该牢记它们只有 7 位和 15 位的有效位数。在交易系统或者科学计算的场景中,如果需要更高的精度小数,可以使用具有 28 个有效位数的 decimal 或者直接使用分数,不过这些表示方法的开销也随着有效位数的增加而提高,我们应该按照需要选择最合适的方法。重新回到今天的问题 — 0.1 和 0.2 相加不等于 0.3 的原因包括以下两个:- 使用二进制表达十进制的小数时,某些数字无法被有限位的二进制小数表示;
- 单精度和双精度的浮点数只包括 7 位或者 15 位的有效小数位,存储需要无限位表示的小数时只能存储近似值;
- 有哪些编程语言内置了高精度的浮点数或者小数?
- 如何实现一个可以精确表示所有实数(包括有理数和无理数)的系统?
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️
评论