大厂如何处理数值精度/舍入/溢出问题

1 计算器的灾难:10%+10%到底等于几?

  • 我们人类以为是 0.2,可是打开手机计算器试试呢?

解密

国外计算程序使用的单步计算法。于是,a+b%表示a(1+b%)。所以,手机计算器实际上在计算10%(1+10%)= 0.11。

再通俗点一句话说清运算原理。以8+10%为例,为什么=8.8而不是8.1?一起读:8元钱,加上10%的小费,一共是8.8元。

最早的电子计算器并没有%,是后来加的。作为后续改进,它一定解决了计算场景中的常用痛点,而绝不是脑残。我推测很可能是西方人计算折扣、小费、利息等常见场景。

2 满目疮痍的Double

  • 浮点数四则运算
  • 结果

由于计算机内部是以二进制存储数值的,浮点数亦是。Java采用IEEE 754标准实现浮点数的表达和运算。比如,0.1的二进制表示为0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是0.1000000000000000055511151231257827021181583404541015625。计算机无法精确表示0.1,所以浮点数计算造成精度损失。

你可能觉得像0.1,其十进制和二进制间转换后相差很小,不会对计算产生什么严重影响。但积土成山,大量使用double作大量金钱计算,最终损失精度就是大量资金出入了。

一位“黑客”利用银行漏洞从PayPal、Google Checkout和其它在线支付公司窃取了5万多美元,每次只偷几美分。他所利用的漏洞是:银行在开户后一般会向帐号发送小额钱去验证帐户是否有效,数额一般在几美分到几美元左右。Google Checkout和Paypal也使用相同的方法去检验与在线帐号捆绑的信用卡和借记卡帐号。 用一个自动脚本开了58,000个帐号,收集了数以千计的超小额费用,汇入到几个个人银行账户中去。从Google Checkout服务骗到了$8,000以上的现金。银行注意到了这种奇怪的现金流动,和他取得联系,Largent解释他仔细阅读过相关服务条款,相信 自己没做错事,声称需要钱去偿还债务。但Largent使用了假名,包括卡通人物的名字,假的地址和社会保障号码,因此了违反了邮件、银行和电信欺骗法律。别在中国尝试,这要判无期徒刑。

3 救世的BigDecimal

我们知道BigDecimal,在浮点数精确表达和运算的场景,一定要使用。不过,在使用BigDecimal时有几个坑需要避开。

  • BigDecimal之前的四则运算
  • 输出
    运算结果还是不精确,只不过是精度高了。

3.1 BigDecimal表示/计算浮点数且使用字符串构造器

阿里华为等大厂如何处理数值精度/舍入/溢出问题
  • 完美输出

无法调用BigDecimal传入Double的构造器,但手头只有一个Double,如何转换为精确表达的BigDecimal?

  • Double.toString把double转换为字符串可行吗?
  • 输出
    401.5000。与上面字符串初始化100和4.015相乘得到的结果401.500相比,这里为什么多了1个0?BigDecimal有scale 小数点右边的位数precision 精度,即有效数字的长度

new BigDecimal(Double.toString(100))得到的BigDecimal的scale=1、precision=4;而
new BigDecimal(“100”)得到的BigDecimal的scale=0、precision=3。

BigDecimal乘法操作,返回值的scale是两个数的scale相加。所以,初始化100的两种不同方式,导致最后结果的scale分别是4和3:

private static void testScale() {
    BigDecimal bigDecimal1 = new BigDecimal("100");
    BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
    BigDecimal bigDecimal3 = new BigDecimal(String.valueOf(100));
    BigDecimal bigDecimal4 = BigDecimal.valueOf(100d);
    BigDecimal bigDecimal5 = new BigDecimal(Double.toString(100));

    print(bigDecimal1); //scale 0 precision 3 result 401.500
    print(bigDecimal2); //scale 1 precision 4 result 401.5000
    print(bigDecimal3); //scale 0 precision 3 result 401.500
    print(bigDecimal4); //scale 1 precision 4 result 401.5000
    print(bigDecimal5); //scale 1 precision 4 result 401.5000
}

private static void print(BigDecimal bigDecimal) {
    log.info("scale {} precision {} result {}", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("4.015")));
}

4 浮点数的舍入和格式化

应考虑显式编码,通过格式化表达式或格式化工具

4.1 明确小数位数和舍入方式

  • 通过String.format使用%.1f格式化double/float的3.35浮点数
  • 结果
    3.4和3.3

精度问题和舍入方式共同导致:double/float的3.35实际存储表示

3.350000000000000088817841970012523233890533447265625
3.349999904632568359375

String.format采用四舍五入的方式进行舍入,取1位小数,double的3.350四舍五入为3.4,而float的3.349四舍五入为3.3。

我们看一下Formatter类的相关源码,可以发现使用的舍入模式是HALF_UP(代码第11行):

阿里华为等大厂如何处理数值精度/舍入/溢出问题

若想使用其他舍入方式,可设置DecimalFormat

阿里华为等大厂如何处理数值精度/舍入/溢出问题

当把这俩浮点数向下舍入取2位小数时,输出分别是3.35、3.34,还是因为浮点数无法精确存储。

所以即使通过DecimalFormat精确控制舍入方式,double/float也可能产生奇怪结果,所以

4.2 字符串格式化也要使用BigDecimal

  • BigDecimal分别使用向下舍入、四舍五入取1位小数格式化数字3.35
  • 结果
    3.3和3.4,符合预期。

最佳实践:应该使用BigDecimal来进行浮点数的表示、计算、格式化。

5 equals做判等就一定对?

包装类的比较要通过equals,而非==。那使用equals对两个BigDecimal判等,一定符合预期吗?

  • 使用equals比较1.0和1这俩BigDecimal:
    结果自然是false。BigDecimal的equals比较的是BigDecimal的value和scale:1.0的scale是1,1的scale是0,所以结果false

若只想比较BigDecimal的value,使用compareTo

阿里华为等大厂如何处理数值精度/舍入/溢出问题

BigDecimal的equals和hashCode会同时考虑value和scale,若结合HashSet/HashMap可能出问题。把值为1.0的BigDecimal加入HashSet,然后判断其是否存在值为1的BigDecimal,得到false

阿里华为等大厂如何处理数值精度/舍入/溢出问题

5.1 解决方案

5.1.1 使用TreeSet替换HashSet

TreeSet不使用hashCode,也不使用equals比较元素,而使用compareTo方法。

阿里华为等大厂如何处理数值精度/舍入/溢出问题

5.1.2 去掉尾部的零

把BigDecimal存入HashSet或HashMap前,先使用stripTrailingZeros方法去掉尾部的零。
比较的时候也去掉尾部的0,确保value相同的BigDecimal,scale也是一致的:

阿里华为等大厂如何处理数值精度/舍入/溢出问题

6 溢出问题

所有的基本数值类型都有超出保存范围可能性。

  • 对Long最大值+1
  • 结果是一个负数,Long的最大值+1变为了Long的最小值
    -9223372036854775808

显然发生溢出还没抛任何异常。

6.1 解决方案

6.1.1 使用Math类的xxExact进行数值运算

这些方法会在数值溢出时主动抛异常。

阿里华为等大厂如何处理数值精度/舍入/溢出问题

执行后,会得到ArithmeticException,这是一个RuntimeException:

java.lang.ArithmeticException: long overflow

6.1.2 使用大数类BigInteger

BigDecimal专于处理浮点数的专家,而BigInteger则专于大数的科学计算。

  • 使用BigInteger对Long最大值进行+1操作。若想把计算结果转为Long变量,可使用BigInteger#longValueExact,在转换出现溢出时,同样会抛出ArithmeticException
  • 结果
9223372036854775808
java.lang.ArithmeticException: BigInteger out of long range

通过BigInteger对Long的最大值加1无问题,但将结果转为Long时,则会提示溢出。

推荐阅读

为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了

字节跳动总结的设计模式 PDF 火了,完整版开放下载

刷Github时发现了一本阿里大神的算法笔记!标星70.5K

程序员50W年薪的知识体系与成长路线。

月薪在30K以下的Java程序员,可能听不懂这个项目;

字节跳动总结的设计模式 PDF 火了,完整版开放分享

关于【暴力递归算法】你所不知道的思路

开辟鸿蒙,谁做系统,聊聊华为微内核

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

关注公众号 『 Java斗帝 』,不定期分享原创知识。

同时可以期待后续文章ing🚀

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

推荐阅读更多精彩内容