Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
51940 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

09 | 数值计算:注意精度、舍入和溢出问题

你好,我是朱晔。今天,我要和你说说数值计算的精度、舍入和溢出问题。
之所以要单独分享数值计算,是因为很多时候我们习惯的或者说认为理所当然的计算,在计算器或计算机看来并不是那么回事儿。就比如前段时间爆出的一条新闻,说是手机计算器把 10%+10% 算成了 0.11 而不是 0.2。
出现这种问题的原因在于,国外的计算程序使用的是单步计算法。在单步计算法中,a+b% 代表的是 a*(1+b%)。所以,手机计算器计算 10%+10% 时,其实计算的是 10%*(1+10%),所以得到的是 0.11 而不是 0.2。
在我看来,计算器或计算机会得到反直觉的计算结果的原因,可以归结为:
在人看来,浮点数只是具有小数点的数字,0.1 和 1 都是一样精确的数字。但,计算机其实无法精确保存浮点数,因此浮点数的计算结果也不可能精确。
在人看来,一个超大的数字只是位数多一点而已,多写几个 1 并不会让大脑死机。但,计算机是把数值保存在了变量中,不同类型的数值变量能保存的数值范围不同,当数值超过类型能表达的数值上限则会发生溢出问题。
接下来,我们就具体看看这些问题吧。

“危险”的 Double

我们先从简单的反直觉的四则运算看起。对几个简单的浮点数进行加减乘除运算:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(23)

  • 最新
  • 精选
  • Darren
    精度问题遇到的比较少,可能与从事非金融行业有关系,试着回答下问题 第一种问题 1、 ROUND_UP  舍入远离零的舍入模式。  在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。  注意,此舍入模式始终不会减少计算值的大小。 2、ROUND_DOWN  接近零的舍入模式。  在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。  注意,此舍入模式始终不会增加计算值的大小。 3、ROUND_CEILING  接近正无穷大的舍入模式。  如果 BigDecimal 为正,则舍入行为与 ROUND_UP 相同;  如果为负,则舍入行为与 ROUND_DOWN 相同。  注意,此舍入模式始终不会减少计算值。 4、ROUND_FLOOR  接近负无穷大的舍入模式。  如果 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同;  如果为负,则舍入行为与 ROUND_UP 相同。  注意,此舍入模式始终不会增加计算值。 5、ROUND_HALF_UP  向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。  如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。  注意,这是我们大多数人在小学时就学过的舍入模式(四舍五入)。 6、ROUND_HALF_DOWN  向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。  如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。 7、ROUND_HALF_EVEN  向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。  如果舍弃部分左边的数字为奇数,则舍入行为与 ROUND_HALF_UP 相同;  如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。  注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。  此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。  如果前一位为奇数,则入位,否则舍去。  以下例子为保留小数点1位,那么这种舍入方式下的结果。   1.15>1.2 1.25>1.2 8、ROUND_UNNECESSARY  断言请求的操作具有精确的结果,因此不需要舍入。  如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。 第二个问题 在MySQL中,整数和浮点数的定义都是有多种类型,整数根据实际范围定义,浮点数语言指定整体长度和小数长度。浮点数类型包括单精度浮点数(float型)和双精度浮点数(double型)。定点数类型就是decimal型。定点数以字符串形式存储,因此,其精度比浮点数要高,而且浮点数会出现误差,这是浮点数一直存在的缺陷。如果要对数据的精度要求比较高,还是选择定点数decimal比较安全。

    作者回复: 👍🏻

    9
    63
  • 👽
    想请教一下。关于金额。 还存在 使用Long类型的分存储,以及封装的money对象存储的方式。这两种方式适合解决金额类的精度丢失问题嘛?

    作者回复: 用分存储是可以(解决精度问题),但是容易出错,万一读的时候忘记/100或者是存的时候忘记*100,可能会引起重大问题,还是使用DECIMAL(13, 2) /DECIMAL(13, 4) 存比较好。

    3
    16
  • Jerry Wu
    感谢老师,看完这篇文章,改了BigDecimal工具类,避免了一个事故。

    作者回复: 赞

    3
    10
  • pedro
    第一个问题,BigDecimal 的 8 中 Round模式,分别是 1.ROUND_UP:向上取整,如 5.1 被格式化后为 6,如果是负数则与直观上不一致,如 -1.1 会变成 -2。2.ROUND_DOWN:向下取整,与 ROUND_UP 相反。 3.ROUND_CEILING:正负数分开版的取整,如果是正数,则与 ROUND_UP 一样,如果是负数则与 ROUND_DOWN 一样。 4.ROUND_FLOOR:正负数分开版的取整,与 ROUND_CEILING 相反。 5.ROUND_HALF_UP:四舍五入版取整,我们直观上最为理解的一种模式,如 5.4 小数部分小于 0.5,则舍位为 5,如果是 5.6 则进位变成 6,如果是负数,如 -5.4 => -5,-5.6 => -6。 6.ROUND_HALF_DOWN:五舍六入版取整,必须大于 0.5 才可进位,其它与 ROUND_HALF_UP 一致。 7.ROUND_HALF_EVEN:奇偶版四舍五入取整,如果舍弃部分左边的数字为奇数,则作 ROUND_HALF_UP;如果它为偶数,则作ROUND_HALF_DOWN,会根据舍弃部分的奇偶性来选择进位的是四舍五入还是五舍六入。 8. ROUND_UNNECESSARY:要求传入的数必须是精确的,如 1 和 1.0 都是精确的,如果为 1.2 或者 1.6 之类的均会报 ArithmeticException 异常。 第二个问题,MySQL 是支持 bigint 和 bigdecimal 数据类型存储的,当然还有 numberic,numberic 的作用与 bigdecimal 一致,当然如果这些数据类型在数据库中计算我觉得是不妥的,应该查询后在代码层面中计算,当然如果有人补充一下如何在数据库中科学计算,也可让大家涨涨见识😄。

    作者回复: 👍🏻

    3
  • 吴国帅
    真棒 get到知识了!

    作者回复: 觉得好可以多转发分享

    2
  • 岳宜波
    一般用的比较多的就是,向上取整,向下取整,四舍五入和舍位四种,在我们项目里因为有国际化,会有币种档案,在币种中定义金额精度和价格精度以及舍入方式,在商品的计量单位上定义数量精度以及舍入方式。

    作者回复: 👍🏻

    1
  • 美美
    请教老师string.valueof替代bigdecimal.valueof可否呢

    作者回复: 可以比较一下,主要是要小心scale: BigDecimal bigDecimal1 = new BigDecimal("100"); BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d)); BigDecimal bigDecimal3 = BigDecimal.valueOf(100d); BigDecimal bigDecimal4 = new BigDecimal(Double.toString(100d)); System.out.println(bigDecimal1.multiply(new BigDecimal("4.015"))); System.out.println(bigDecimal2.multiply(new BigDecimal("4.015"))); System.out.println(bigDecimal3.multiply(new BigDecimal("4.015"))); System.out.println(bigDecimal4.multiply(new BigDecimal("4.015")));

    1
  • 珅珅君
    我想补充一点,之所以DecimalFormat也会导致精度的问题,是因为 format.format(num) 这个方法参数是double类型,传float会导致强转丢失精度。所以无论怎么样,浮点数的字符串格式化通过 BigDecimal 进行就行

    作者回复: 不错

  • Monday
    手机计算器把 10%+10% 算成了 0.11 而不是 0.2。 读到这里,吓得我赶快掏出安卓机算了下
    11
  • Geek_3b1096
    用equals对两BigDecimal判等...之前就被坑了
    2
    9
收起评论
显示
设置
留言
23
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部