double精度丢失问题
从二进制角度考虑问题就会很简单了:把「0.1」转成二进制然后还原成十进制,就能看出问题。
- 这是二进制与十进制之间的碰撞
- 这是机器与人们之间的差异
- 这是思想与结果的碰撞
- 理解之后一切都会变得合理……毕竟称之为浮点数
一、把 0.1 转成二进制表示
我们知道 DEC(1) 就是 BIN(1),但是 DEC(0.1) 怎么转换成二进制?
是的!用除法:
0.1 = 1 ÷ 10
很简单,二进制就是要算
1 ÷ 1010
我们回到小学的课堂,来列竖式吧:(算了还是用计算器来的更快些……)
0.000110011...
/------------------
1010 / 1 0000
/ 1010
------
1100
1010
----
10000
1010
-----
1100
1010
----
10
这时候你一定会发现,除不尽,除出了一个无限循环小数:二进制的 0.0001100110011…
二、机器语言的舍入
我们得把 0.0001100110011… 放进一个 double「双精度浮点数」里面,机器容量有限,不可能存储无限循环小数,只能存储部分
双精度浮点数能表示多少精度呢?查看文档会发现:
- 半精度(16bit) : 11 位有效数字
- 单精度(32bit) : 24 位有效数字
- 双精度(64bit) : 53 位有效数字
- 四精度(128bit):113 位有效数字
好吧,双精度是 53 位有效数字
0.00011001100110011001100110011001100110011001100110011001 10011...
方便起见,我在第 53 个有效数字后面加了空格。有点难数,使用编程打印的
十进制数我们可以四舍五入,二进制怎么办?
- 精神是一样的:待处理部分有没有达到前一位的一半,达到就进位,没达到就舍去。(暂且当作 0 舍 1 入。)
那么我们的 0.1 在 double 中就是
0.00011001100110011001100110011001100110011001100110011001 10011...
0.00011001100110011001100110011001100110011001100110011010
而 1.1 就是
1.0001100110011001100110011001100110011001100110011001 10011...
1.0001100110011001100110011001100110011001100110011010
三、机器语言的加法
这个很简单,1.1 + 0.1 就是 1.2, 但是让我们看看机器如何相加↓:
1.0001100110011001100110011001100110011001100110011010
+ 0.00011001100110011001100110011001100110011001100110011010
------------------------------------------------------------
1.00110011001100110011001100110011001100110011001100111010
因为结果仍然是 double,需要再做一次保留 53 位有效数字和舍入:
1.0011001100110011001100110011001100110011001100110011 1010
1.0011001100110011001100110011001100110011001100110100
四、二进制转十进制的偏差
机器计算完成,机器需要转十进制给我们看
我们把最终结果转换回来:
1.0011001100110011001100110011001100110011001100110100
得到十进制的:
1.20000000000000018
一般的输出函数,在输出浮点数时,都会限制显示的有效数字,即会再做一次四舍五入。
**PS:**至于各个语言之间的差异,答案是可能会有,比如可能因为选择的舍入规则的不同可能导致的结果的不同;甚至有可能某个语言里的浮点数压根不是 IEEE 754 的浮点数,而是以字符串方式保存的,所以可能没有误差。
Double的输出
//使用BigDecimal
@Test
public void DoubleTest() {
double testDoubleVal = 1.23456789;
BigDecimal bigDecimal = new BigDecimal(testDoubleVal);
System.out.println(bigDecimal.setScale(3, BigDecimal.ROUND_HALF_UP));
System.out.println(bigDecimal.setScale(4, BigDecimal.ROUND_HALF_UP));//不同的舍入规则
System.out.println(bigDecimal.setScale(4, BigDecimal.ROUND_DOWN)); //不同的舍入规则
}
/*
运算结果:
1.235
1.2346
1.2345
*/