浮点数计算误差解析

浮点运算

近期因为在公司的新系统刚上线不久,还在每日填坑监控中,所以会定期拉取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:(毕竟我们日常生活中还是以十进制为生活的维度)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容