记录学习Double转Bigdecimal丢失精度的原因
注意事项:
不能直接使用Bigdecimal的构造函数传double进行转换,部分数值会丢失精度,因为计算机是二进制的Double无法精确的储存一些小数位,0.1的double数据存储的值实际上并不真的等于0.1
如该方式将0.1转换为Bigdecimal得到的结果是
0.1000000000000000055511151231257827021181583404541015625
这是为什么呢,以往只是知道结论知道不能这么用,也大概知道是因为double是双精度导致的,但是没有太关注原因。这次就来进一步学习一下
首先给出Double转BIgdecimal的常用方式
1、可以手动先将Double转换为String再转换为Bigdecimal 则不会发生精度丢失问题
BigDecimal bigDecimal = new BigDecimal(String.valueOf(0.1));
2、可以直接调用Bigdecimal的函数
BigDecimal bigDecimal = BigDecimal.valueOf(0.1);
这个函数跟一下源码内部其实也是先将Double转为String
public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}
接下来我们找一下会出现丢失精度的原因
首先要知道计算机语言是二进制语言,而0.1是我们日常十进制的言语。
二进制与十进制的转换比较简单。大多数时候不需要我们手动去计算,但还是可以学习一下。网上的在线转换工具也很多,这里不详细介绍了
第二个要知道Double的数据格式,Double是双精度,Float是单精度。
Double与Float的数据格式是一致的,但是长度不同。数据分为三段
Double | ----符号位长度1-- | ------指数位长度8--------| --------尾数长度23-----------
Float | ----符号位长度1-- | ------指数位长度11-------| --------尾数长度52-----------
第一段也就是第一位是符号位,然后第二段是指数位,第三段是尾数
符号位
符号位好理解 十进制为正数则 = 0 负数 = 1
比如0.1与-0.1的Float内存数据:
0.1 = 00111101110011001100110011001101
-0.1 = 10111101110011001100110011001101
指数位
指数位存储的是转换为二进制数值后类似再转换为科学计数法的指数数值
指数位长度是8。8位二进制正常的范围值为0~255。但是十进制的小数的对应的指数位可能为负数,为了方便记录所以规定指数位的指数偏移 Float+127,Double+1023 后再转换为二进制。
注意这里指数位存储的不是十进制科学计数法的指数,而是二进制的指数值。我们以0.1为例
- 错误的示例 0.1(10) = 1 * 10-1 十进制科学计数法指数位 -1 + 127 = 126(10)= 01111110(2)然而指数位不是存储这个数值01111110 。0.1对应正确的指数位是应该是 01111011(2)= 123(10)
为什么呢?我们和尾数一起学习一下
尾数位
尾数位存储的是数值转换为二进制后的类似科学计数法的二进制数的基数。我们还是以0.1为例
先将0.1转换为二进制,方法我们不详细介绍,0.1的计算大致可以乘以2取整直到结果为0
- 0.1 * 2 = 0.2 小数位继续计算 二进制取整数位: 0
- 0.2 * 2 = 0.4 小数位继续计算 二进制取整数位: 0
- 0.4 * 2 = 0.8 小数位继续计算 二进制取整数位: 0
- 0.8 * 2 = 1.6 小数位继续计算 二进制取整数位: 1
- 0.6 * 2 = 1.2 小数位继续计算 二进制取整数位: 1
- 0.2 * 2 = 0.4 小数位继续计算 二进制取整数位: 0 开始重复
- 0.4 * 2 = 0.8 小数位继续计算 二进制取整数位: 0 重复
- …
- …
一直算下去得到就是一个无限循环数
- 0.1(10)= (2) 0 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011…
这就是0.1对应的二进制,类似于十进制中的 1/3 = 0.33333…是个无限数而Float(32)与Double(64)长度是有限的是无法精确表示出这个数值的,只能是无限接近0.1。有兴趣可以手动计算它等于0+0+0+0.0625+0.0315+0.015625…> 0.1 而一直加下去但是永远不可能等于0.1
再转换为科学计数法移动三位小数点 :
- 0.1(10)= (2) 0 0011 0011 0011 0011 0011 0011 0011 0011… = (2^)1.1001100110011…* 2-3
所以0.1Float指数位值是
- -3+127(偏移值 )= 123(10) = 1111011(2) 补齐8位存入指数位就是 01111011
那么尾数呢,尾数位本应该是110011001100110011…
但是因为科学计数法正数首位一定为1,所以二进制存储时不存储这个数值,只是在计算过程里加上即可。所以去除首位的1 最终尾数位存储的就是
- Float: 10011001100110011001101
Double: 1001100110011001100110011001100110011001100110011010
合在一起最终0.1的储存数据为
- 0.1 -> Float :0 01111011 10011001100110011001101
0.1 -> Double:0 01111111011 1001100110011001100110011001100110011001100110011010
对应-0.1则只需要把首位的符号位改为1
- -0.1 -> Float :1 01111011 10011001100110011001101
计算逻辑
我们再整理一下数据的存储逻辑
- 符号位判断十进制数正负 赋值 (正数:0、负数:1) 存入符号位
- 将十进制转换为二进制数
例:2.2(10) = 100011001100110011001101… - 将二进制数转换为二进制的科学计数法表达
例 : 2.2(10) = (2) 1.00011001100110011001101… = (2)1.00011001100110011001101… * 21 - 指数值偏移(Float+127、Double+1023)得出十进制的指数 再转换为二进制数 存入指数位
例:(2)1.00011001100110011001101… * 21 —— 偏移——-> 1+127或1023 = 128或1024 ———转为二进制——->10000000或10000000000 - 将得出的二进制科学计数法基数去除首位的1(因为这个值是固定为1的)存入尾数位
例:1.00011001100110011001101 —-去除首位的1—–> 00011001100110011001101
最终结果
2.2 —Float—> 01000000000011001100110011001101
2.2 —-Double —->0100000000000001100110011001100110011001100110011001100110011010
搞明白精确丢失的原因,就是因为有限的二进制无法准确的存储一些数值如:0.1/0.2。那么自然有数值也是能精确存储的。可以直接使用new Bigdecimal(Double d)并且不会丢失精度,那么什么样的数值使用构造方式不会丢失精度呢?
可以找几个数值动手算一下加深理解
今天的文章Java Double转Bigdecimal丢失精度原因学习分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/9315.html