此文深入剖析困扰哥已久的浮点数表示和运算。
定义
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
。