0.1 + 0.2 = 0.30000000000000004?从进制转化、存储再到BIGDECIMAL!!!

1,646 阅读4分钟

引文

最近在自学python,因此又重新接触了很多基础知识,
在偶然间发现0.1 + 0.2 = 0.30000000000000004,
发现了这个既日常但又被我忽视的现象(在JAVA中同样成立)
因此,就很好奇地探索了一下。

Python中

image.png

JAVA中

image.png

image.png

产生原因

十进制数转化为二进制数

我们计算的0.1、0.2包括其他都属于十进制数,得经过转化为二进制,才会进行存储和计算。
接下来,我们分两部分看看是如何转化的。

整数转化

image.png 以10为例子,我们首先除于2,得到余数0,商为5
此时的5代表了第二位的大小,第二位的位大小根据我们刚刚除二,反推等于1×2 = 2
目前 10 = 0×1 + 5×2
我们接着除于2,得到余数1,商为2
此时的商2代表了第三位的大小,第三位的类推等于1×2×2 = 4
更新为 10 = 0×1 + 1×2 + 2×4,
以此类推,直到我们的商为0,则结束
我们的十进制数伴随着不断地被除,余数只会越来越小,而且一定可以被完成计算和表示

小数转化

image.png
小数部分,如果我们在二进制的某一位存在1,那么我们乘以2的位数次方,结果的整数部分会大于1
如 010b = 0.25,第二位存在1,乘数也就是2的2次方 ,0.25×2² = 1
根据这个原理,我们每次乘以二,记下整数位为当前位的数字(0、1),
记录下积的小数部分,再次乘以二,做相同的递归处理,直到积为0
以0.625为例子,0.625×2 = 1.25> 1 ,说明该值大于0.5,取整数位为1,取积的小数部分0.25
再把0.25 × 2 = 0.5 < 1
取整数位为0 , 取积的小数部分0.5(此时的积的小数部分已经被乘了2²)
最后0.5 × 2 = 1.0 = 1 小数部分为0,即为结束

好,那么尝试计算一下0.1,0.2对应的二进制数
0.1 = 0.00011001100……(无限重复1100)
0.2 = 0.001100110011……(无限重复0011)
都是无限循环的小数
从推理过程来看,0.1 -> 0.2 -> 0.4 -> 0.8 -> 0.6 -> 0.2,
小数部分计算后回到了原点,导致了循环,存在一些小数乘以的2任意次方都无法使得小数位为0
既然是无限循环,计算机的空间是有限的,无论采取何种存储策略,必定只能截取部分数据
也就是精度丢失了

浮点数存储(IEEE 754)

把十进制转化为二进制后,我们实现是通过浮点数来存储二进制数据
首先通过科学计数法表示 10.625 = 1010.101 = 1.010101 × 10^-4
接下来只需要按三部分存储数据,存储正号、10的次方数(4)、以及1后的小数点数据(010101) 浮点数存储.png 分别对应了符号位、指数部分、以及尾数部分 image.png (图片来源zhuanlan.zhihu.com/p/372019872)

我们再看看0.1的存储

image.png 0.1 = 0.00011001100……(无限重复1100),
前面提到了无限小数被截取,本应该最后一位为0,此处却为1,
这是因为被截取的下一位为1,1100 截断 1100
这里为了更贴近原值而零舍一入,最后四位于是变成了1101
所以最终存储的值是比原值稍大
image.png 0.2 = 0.001100110011……(无限重复0011) ,也是相同的情况 image.png

作为对比我们看看零舍的0.9
image.png 0.9是直接截断,等于丢失了一部分尾部的数据,所以存储值稍小于原值0.9

总结

小数在十进制转化二进制的过程会可能产生无限循环的情况,
而且根据浮点数的零舍一入的法则,最终存储的数据都不是一个准确的数据,可能或大或小
0.1和0.2的存储值都是比原值稍大,所以最终的值也是稍大于原值

解决办法

前面我们介绍到整数是可以在有限的空间存储的性质,小数的存储是导致不准确的诱因,
那么我们去掉小数,全部转化为整数存储计算!

BigDecimal

image.png
我们分别通过字符串和字面量传入BigDecimal,查看结果

image.png

可以看到,通过字符串传入的结果符合我们的预期,而直接传入的结果仍然存在偏差。

image.png
直接传入的内部实现

image.png
通过字符串传入的实现

实现原理

经过前面的过程,我们可以看出
Bigdecimal是通过输入字符串,而非浮点数(浮点数本身就存在偏差),
就是通过去掉小数点,用一个scale来记录小数的位数,把所有的数字用long来记录,实现精确计算。

public class BigDecimal  {
// 小数位数,类似于指数
private final int scale;
// 总位数
private transient int precision;
// 字符串缓存
private transient String stringCache;
// long存储的整形数据
private final transient long intCompact;

}

使用BigDecimal解决0.1+0.2问题

代码

image.png

结果

image.png

结尾

网上类似的文章有很多,
这里以个人的理解尽可能从基础的角度去阐释这个问题,希望对你有所帮助!
END!