此文深入剖析困扰哥已久的浮点数表示和运算。
定义
Java 浮点数定义采纳 IEEE Standard 754 标准:单精度 float 32 位,双精度 double 64 位。本文主要以 float 为例。

- 最高位符号位
- 0 正 1 负
- 接 8 位指数位,并有 127 的偏移量
- 所以指数范围为:0 - 127 ~ (28 - 1) - 127
- 全 0 和全 1 保留用特殊表示,所以指数域的修正范围为 -126 ~ 127
- 剩下 23 位为尾数域
- IEEE 要求浮点数必须是规范的,即小数点的左侧必须为1,这样腾出了一个二进制位来保存更多的尾数,即我们用 23 位尾数域表达了 24 位的尾数
- 所以尾数域上限为 224 - 1,即 0 ~ 16777215
- 107 < 16777215 < 108,所以
float可以精确到小数点后 7 位(存疑)

- 特殊表示
- 指数全为 0,尾数为 0 时,表示 0
- 指数全为 1,尾数为 0 时,表示无穷大
- 指数全为 1,尾数不为 0 时,表示
NaN
示例图中的数字表示解析:
- 符号位 0 ,表示正数
- 指数位
01111100,表示 26 + 25 + 24 + 23 + 22 - 127 = -3 - 尾数域左侧补 1 位,表示
101 - 最后值为 1 * 2-3 + 1 * 2-5 = 0.15625
精度
1 2 3 4 5 6 | |
输出:
1 2 3 | |
示例中的 c 精确到小数点后 10 位,可以精确的由二进制表示:1.0009765625 = 20 + 2-10。
- 第一行输出为其二进制表示,进行验证
0 01111111 00000000010000000000000- 指数:27 - 1 - 127 = 0
- 尾数:1.0000000001 = 20 + 2-10
以上输出的第二行表示 Java 对 float 处理的默认精度为 7 位,但是这不表示它的存储就丢失了精度。输出的第三行加入了指定的精度,即得到了无精度损失的浮点数。
所以这里得到我的个人结论: float 的 7 位精度是规约,而不是表示结构的限制。
二进制表示方法
0.1 = 1.6 / 16
= 1 / 16 + 0.6 / 1
= 1 / 16 + 1.2 / 32
= 1 / 16 + 1 / 32 + 0.2 / 32
= 1 / 16 + 1 / 32 + 1.6 / 28
= 1 / 24 + 1 / 25 + 1 / 28 + 0.6 / 28
= …
第 6 步又回到了第 2 步一样的分子 0.6 ,所以这是一个无限循环小数
0.1 = 0.00011001 00011001 00011001 00011001…
场景
Puzzle 2
1 2 3 | |
这个简单的算式得到的结果不是期望的 0.9 ,而是 0.8999999999999999 。因为 1.1 不能被精确的保存为 double 类型,而被保存为了最接近 1.1 的值,不幸的是,这个值与 2.0 做减法运算后得到的不是最接近 0.9 的 double 值,而是输出的这个奇葩数。
1 2 3 | |
秘籍:需要精确表示时,用 BigDecimal(String str) ,永远不要用浮点数做运算。
Puzzle 28: Looper
1 2 3 | |
给 i 一个声明,使上面的语句进入无限循环状态。。
1 2 3 4 5 | |
无穷大不用说。因为浮点数不能精确保存值,当一个数很大时,它的后继邻接数 (ulp) 与其差值可以大于 1 。
The distance between adjacent floating-point values is called an ulp, which is an acronym for unit in the last place. In release 5.0, the Math.ulp method was introduced to calculate the ulp of a float or double value.
秘籍:不要用浮点数做循环索引。
Puzzle 87: Strained Relations
数学上对于 = 的定义满足相等关系 (equivalence relation) 的三个条件
- 自反性:x ~ x for all x.
- 对称性:if x ~ y, then y ~ x.
- 传递性:if x ~ y and y ~ z, then x ~ z.
那么 Java 中的 == 呢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
总结
注意浮点数的精度丢失以及类型转换,相对于 float ,优先用 double 。