浮点数计算误差解析

浮点运算

近期因为在公司的新系统刚上线不久,还在每日填坑监控中,所以会定期拉取error级别日志来观察系统稳定,在今日拉取的错误日志中发现一条error日志,通过定位分析到是下面一段代码抛出的断言异常:

    val card1 = 3.33
    val card2=4.44
    val totalBalance = 7.77
   
    assert(card1+card2 == totalBalance, illegalArgumentException("数据异常"))

通过debug得知在card+card2两个double类型相加得出来的值是7.7700000000000005;其实这是在程序在进行基本类型运算中,对于基本类型不够了解造成的误会,那么接下来我们来看看基本类型在计算机中到底是怎么进行类型计算的

Double类型与float精度类型

double:


微信图片编辑_20180721160222.jpg

我们可以从wiki官方可以看到double类型在计算机中Double型数据根据IEEE754标准,占用计算机64bit,即8个字节的内存空间,浮点(小数位)占用52bit [bit 0 -51], 指数位占用(11bit) [bit 52 - bit 62], 符号位1bit(bit 63)

于是在我们给基础类型计算时,计算机中都是通过转换成2进制来转换,以上述为例,

val a=3.333  val b=4.44 
3.33+4.44

从二进制换算从做左到右换算,ps:(这还是努力搜索的结果,二进制换算已经都还给大一的学堂了)

整数型
3为整数型即21
浮点数:
0.33*2=0.66取整数部分0,基数=0.66
0.66*2=1.32取整数部分1,基数=0.32

以此类推,直到基数为0
那么 上述图中double为52位,当无穷小数超出位数后则会自动舍弃掉,最终结果就为7.7733301001..........那么就肯定不与7.77相等了

于是小编立即想到了另一个平时大范围运用到的基本类型float,那么float是不是也会跟double一样呢,通过下述实验可以看出

    public static void main(String[] args) {
        float a=3.33f;
        float b=4.44f;
        System.out.println(a+b);
        float c=3.333333333333111f;
        float d=4.444444444444222f;
        System.out.println(c+d);
    }

console:

D:\Java\jdk1.8.0_151\bin\java
7.77
7.7777777

Process finished with exit code 0

那么是不是很奇怪呢,为什么到float类型的时候有时候是好的有时候不行呢
其实我们不难发现,float的长度并没有double长,他的尾数只有23bit,那么后面的都会自动舍弃掉。

总结与解决方案

其实大部分coder包括小编自己,在毕业之后一直尊崇着运算变量必须使用decimal类型,包括还记得是刚工作两年的时候反复阅读《Effcitve java》中也提到double与float只能用做工业数字展示,商业运算必须使用bigdecimal,double与float会产生精度问题,那么bigdecimal为什么可以避免上述问题呢

我们可以看下bigdecimal在运算上述中的结果:

       BigDecimal  g=new BigDecimal(3.33f);
       BigDecimal y=new BigDecimal(4.44f);
       BigDecimal u=g.add(y);
        System.out.println(u);
        BigDecimal t=g.add(y).setScale(2,BigDecimal.ROUND_UP);
        System.out.println(t);

console如下

D:\Java\jdk1.8.0_151\bin\java 
7.769999980926513671875
7.77

Process finished with exit code 0

那么我们可以从上述看出,decimal类型其实他并不是在原变量赋值计算,他会计算后赋值给新变量,并且如果不指定保留小数位以及四舍五入的参数,一样会产生误差,那么我们来看看bigdecimal的方法实现

 private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
        long sdiff = (long) scale1 - scale2;
        if (sdiff == 0) {
            return add(xs, ys, scale1);
        } else if (sdiff < 0) {
            int raise = checkScale(xs,-sdiff);
            long scaledX = longMultiplyPowerTen(xs, raise);
            if (scaledX != INFLATED) {
                return add(scaledX, ys, scale2);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
                return ((xs^ys)>=0) ? // same sign test
                    new BigDecimal(bigsum, INFLATED, scale2, 0)
                    : valueOf(bigsum, scale2, 0);
            }
        } else {
            int raise = checkScale(ys,sdiff);
            long scaledY = longMultiplyPowerTen(ys, raise);
            if (scaledY != INFLATED) {
                return add(xs, scaledY, scale1);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
                return ((xs^ys)>=0) ?
                    new BigDecimal(bigsum, INFLATED, scale1, 0)
                    : valueOf(bigsum, scale1, 0);
            }
        }
    }

我们从上面方法中可以看到,add方法其实他也是没有改变核心原理,在转换成二进制之后进行递归计算,但是他提供了 封装的保留位数以及四舍五入的方法,所以在运用中给商业计算带来了可控的保险,当然因为他的实现,我们可以看到其开销也是不小的,new了新的变量,以及long的位数值,double浮点数的转换类型。

综上,我们在商业特别是跟有小数点相关的场景中,一定要使用bigdecimal类型进行赋值运算,并且在系统设计中就要统一制定好小数位保留长度以及四舍五入策略,这样在代码中统一遵照就可以避免计算机在二进制运算带来的误差,ps:(毕竟我们日常生活中还是以十进制为生活的维度)

你可能感兴趣的:(浮点数计算误差解析)