浮点数精度丢失分析及解决

浮点数精度丢失分析及解决,第1张

浮点数精度丢失分析及解决

Java语言在处理浮点数,其实现逻辑与整数不同,如果使用不当可能会造成精度丢失、计算不准确、死循环等问题,严重的话,会造成经济损失。本文将从浮点数精度丢失入手,详细介绍下浮点数的原理及使用。

为什么会出现精度丢失

计算机使用二进制存储数据,由于二进制自身局限性,导致其无法精确的表示所有小数。而浮点数是由整数部分和小数部分组成,这也就意味着,计算机无法精确表示浮点数。也即,在计算机中,浮点数存在精度丢失的问题。这里将以十进制小数转为二进制数为例,介绍下为何二进制无法精确表示小数。
十进制小数转换成二进制小数采用"乘2取整,顺序排列"的做法。基本思想如下:用二乘以当前十进制小数,然后将乘积的整数部分取出,如果乘积中的小数部分为零或者达到所要求的精度则停止计算。否则再用二乘以余下的小数部分,又得到一个乘积,再将积的整数部分取出,如此进行。完成计算后,把每次执行取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。这里介绍下如何把十进制小数0.8125转换为二进制小数。

当然,也存在无法准确适用二进制表示的小数。如十进制小数0.7。

对于无法用二进制表示的小数,只能根据精度近似的表示。

浮点数底层存储实现

与整数存储数值不同,浮点数在计算机的存储时,会拆分成是三个部分:符号位、指数位、尾数位。其抽象公式是:
( − 1 ) S ∗ ( 1. M . . . ) ∗ 2 E (-1)^S*(1.M...)*2^E (−1)S∗(1.M...)∗2E
其中, S S S表示符号(正数、负数)、E表示指数、M表示尾数。在Java中,float是32位存储,double是64存储,各部分的存储长度是:


因为指数位影响数的大小,指数位决定大小范围。而小数位则决定计算精度,小数位能表示的数越大,则能计算的精度越大。
float 的小数位只有 23 位,即二进制的 23 位,能表示的最大的十进制数为 2 的 23 次方,即 8388608,即十进制的 7 位,严格点,精度只能百分百保证十进制的 6 位运算
double 的小数位有 52 位,对应十进制最大值为 4 503 599 627 370 496,这个数有 16 位,所以计算精度只能百分百保证十进制的 15 位运算。

浮点数 *** 作

既然浮点数不能精确表示小数,那么在执行浮点数 *** 作时(算数运算、比较运算等),要考虑精度丢失可能带来的问题。这里以浮点数比较为例,介绍两个浮点数比较带来的死循环问题。

public void createEndlessLoop() {
    double a = 1.6;
    double b = 0.3;
    double c = a + b;
    double d = 1.9;
    while (c != d) {
        System.out.println("c: " + c + ", d: " + d);
    }
    System.out.print("c == d");
}

执行上述方法,该方法将进入到死循环。浮点数的比较,由于精度丢失问题,其比较结果常常与预期相左。一种基本的思路是引入误差来辅助浮点数比较,但是这种做法仍不能满足某些场景。更多该思路的拓展可以参考链接。

如何避免精度丢失

那么如何避免浮点数的精度丢失问题。其实这个问题没有银d。要根据不同的业务场景,选择合适的处理方式。如果可以不对浮点数进行运算,则尽量不使用浮点数进行运算。如果必须使用浮点数进行运算,要根据场景,选择合适的处理方式。如浮点数四则运算、比较运算场景,使用BigDecimal。上一个问题的正确处理方式如下:

public void createEndlessLoopWithBigDecimal() {
    BigDecimal a = new BigDecimal("1.6");
    BigDecimal b = new BigDecimal("0.3");
    BigDecimal c = a.add(b);
    BigDecimal d = new BigDecimal("1.9");
    while (c.doublevalue() != d.doublevalue()) {
        System.out.println("c: " + c.doublevalue()  + ", d: " + d.doublevalue());
    }
    System.out.print("c == d");
}

注意,这里并没有使用形如"BigDecimal a = new BigDecimal(1.6);"的方式初始化BigDecimal实例,这是因为浮点数无法精确表示,所以使用BigDecimal(Double)创建的BigDecimal是可能损失了精度,其赋值结果可能和实际有差异。考虑下面的代码,将进入死循环。

public void createEndlessLoopWithBigDecimal() {
    BigDecimal a = new BigDecimal(1.6);
    BigDecimal b = new BigDecimal(0.3);
    BigDecimal c = a.add(b);
    BigDecimal d = new BigDecimal(1.9);
    while (c.doublevalue() != d.doublevalue()) {
        System.out.println("c: " + c.doublevalue()  + ", d: " + d.doublevalue());
    }
    System.out.print("c == d");
}

更多BigDecimal使用上的坑,可参考链接。

参考

https://www.cnblogs.com/fuzongle/p/14986543.html Java浮点数计算精度丢失与解决方案
https://www.cnblogs.com/xujishou/p/7491932.html java中double和float精度丢失问题
https://www.runoob.com/w3cnote/decimal-decimals-are-converted-to-binary-fractions.html 十进制小数转化为二进制小数
https://blog.csdn.net/qq_36915078/article/details/106019023 浮点数如何转二进制
https://www.runoob.com/w3cnote/java-the-different-float-double.html Java 浮点类型 float 和 double 的主要区别
https://www.cnblogs.com/PrimoPrimo/archive/2013/02/22/3307196.html 浮点数的比较
https://developer.aliyun.com/article/785039 BigDecimal使用避坑

原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5637196.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存