V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Gct012
V2EX  ›  程序员

关于 Java 的一个神奇现象,有没有人可以解答一下的...

  •  
  •   Gct012 · 2023-05-30 16:58:16 +08:00 · 2964 次点击
    这是一个创建于 541 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 Java 开发过程中,使用 bigdecimal 记录那些小数点后 N 位的数字,但是在处理比如(1 / 3 * 6)这样的场景时,得到的结果会是 1.999999999999998 。但是在 Oracle 数据库或者是 windows 自带的计算器中,使用 0.33333333333333333 * 6 ,得到的结果均是 2 。想问下 Java 在这一块有什么办法可以做到和其他两个一样的结果么?

    21 条回复    2023-05-31 13:09:12 +08:00
    yule111222
        1
    yule111222  
       2023-05-30 17:06:14 +08:00
    java 这个才是正常现象好么一点也不神奇,不如说 Oracle 数据库或者是 windows 自带的计算器中,使用 0.33333333333333333 * 6 ,得到的结果均是 2 才是异常的。盲猜一下不一定准是计算器缓存了前面的除法表达式,然后如果后面有乘法就先做乘法运算,再做除法运算
    cc666
        2
    cc666  
       2023-05-30 17:06:18 +08:00
    解:不用 bigdecimal 即可
    jshell> 0.33333333333333333 * 6
    $2 ==> 2.0
    shanch
        3
    shanch  
       2023-05-30 17:07:13 +08:00
    你用其他计算器先计算 1 / 3 等于的值 * 6 试试
    mineralsalt
        4
    mineralsalt  
       2023-05-30 17:07:25 +08:00
    我试了一下, java 也是 2, 不知道你是怎么计算的

    BigDecimal bd = new BigDecimal(1.0 / 3.0 * 6);
    System.out.println(bd.toString());

    结果: 2.0
    cc666
        5
    cc666  
       2023-05-30 17:11:03 +08:00
    PS1 ,这没啥神奇的,基本的计算机组成原理
    PS2 ,你问题中,1 / 3 * 6 和 0.33333333333333333 * 6 根本不是等价的
    PS3 ,经验证,windows 自带的计算器,0.33333333333333333 * 6 = 1.99999999999999998 ,win11 22H2
    Gct012
        6
    Gct012  
    OP
       2023-05-30 17:12:19 +08:00
    @mineralsalt 啊....不是,业务需求是先算除法再算的乘法,BigDecimal a = new BigDecimal("1");BigDecimal b = new BigDecimal("3");BigDecimal c = new BigDecimal("6"); a.divide(b,12,RoundingMode.HALF_UP).multiply(c),这样就是 1.9999999999998 了
    zjsxwc
        7
    zjsxwc  
       2023-05-30 17:13:44 +08:00
    换 float 运算 1.0/3*6 结果都是 2

    换限制了 scale 小数点位数的大数运算 "1.0 " / "3" * "6" 结果都是 "1.999..98"
    cc666
        8
    cc666  
       2023-05-30 17:14:34 +08:00
    @shanch #2
    试了一下也是,不知道楼主怎么算出 1.999999999999998 的
    Windows 计算器计算 0.33333333333333333 * 6 的结果是 1.999999999999998
    Gct012
        9
    Gct012  
    OP
       2023-05-30 17:15:56 +08:00
    @cc666 我也是 win11 ,自带计算器用 0.33333333333333333 * 6 得到的就是 2 ,用的标准的不是科学计算器........
    zjsxwc
        10
    zjsxwc  
       2023-05-30 17:16:48 +08:00
    本质问题是 限制了 scale 的 除法 必然会丢失精度。
    LongerAng
        11
    LongerAng  
       2023-05-30 17:17:35 +08:00
    @mineralsalt 兄弟,不是你这样算的。你这和直接输出 1.0 / 3.0 * 6 不是一样的吗。浮点数计算最好调用方法
    Gct012
        12
    Gct012  
    OP
       2023-05-30 17:19:35 +08:00
    cc666
        13
    cc666  
       2023-05-30 17:23:18 +08:00
    @Gct012 #6 a.divide(b,12,RoundingMode.HALF_UP).multiply(c) 浮点数的存储方式无法精确表示 1/3 ,你这限制了 scale 又设置了 RoundingMode 进位,肯定丢失京都了
    oldshensheep
        14
    oldshensheep  
       2023-05-30 18:03:28 +08:00
    你不用 bigdecimal 使用的是浮点数,计算不精确,溢出的位数会 round ,也就是 1.999999999999999998 ( 9 的个数是乱打的)这个数表达不了,进位了所以得到了 2.0

    而是用 bigdecimal 是精确计算,1/3 是无限循环会要设置精度,所以都是精确计算。
    你要做到一样就把结果转成 double 吧……
    urnoob
        15
    urnoob  
       2023-05-30 18:55:42 +08:00 via Android
    @mineralsalt
    您这写不对啊。。。在变 bigdxxxx 前就已经算好了结果。。。
    qwerthhusn
        16
    qwerthhusn  
       2023-05-30 19:31:22 +08:00   ❤️ 1
    看了这么多评论,我感觉好像我也没那么菜,找工作的信心又增加了一分。
    luhe
        17
    luhe  
       2023-05-30 19:33:14 +08:00 via iPhone
    业务要求先算除法保留小数位,即使丢失精度也在所不惜么,这个小数位给你提需求具体是多少了么
    nothingistrue
        18
    nothingistrue  
       2023-05-31 09:48:17 +08:00   ❤️ 1
    @Gct012 Bigdecimal (1) / Bigdecimal (3) * Bigdecimal (6) ,1 / 3 * 6 ,1.0 / 3 * 6 ,这在计算逻辑上就是三码事,你就不该把他们混为一谈。这是计算基础的问题,跟 Java 都没关系。

    Bigdecimal (1) / Bigdecimal (3) * Bigdecimal (6),这是精确的十进制数字计算(现实世界的计算规则),在除法结果面对无限小数的时候,必定要做舍入。你的除法里面选择的规则是「精度 12 ,四舍五入」,因此 1/3 的结果是 0.333333333333 ,而最后结果就精确的为 1.999999999998 。

    1 / 3 * 6 是整型计算,在除法结果面对小数的时候,要丢弃。故 1/3 的结果是 0 ,最终结果是 0

    1.0 / 3 * 6 因为 1.0 的原因,被调整成了浮点计算,浮点数和浮点计算,是计算机专用的计算逻辑,与现实世界的计算规则有所不同,经常出现一些莫名其妙的错误,比如 0.33333333333333333 * 6 = 2 。


    计算机默认的计算规则就是浮点计算,十进制计算是需要特殊处理的。Windows 计算器里面,科学计算器是高规格的精确结算,标准计算器是快速计算,这俩是不一样的。如果你想要结果都是 0.33333333333333333 * 6 = 2 ,那就别用 BigDecimal ,用 Double 或者 基本类型 double 。但是请不要这么干,原因去找初中的数学教科书。
    newaccount
        19
    newaccount  
       2023-05-31 12:46:30 +08:00
    调整计算顺序,先计算乘法。需要设置 setScale 的放到最后计算,只需要保证数学上相等就行,精度问题无解
    mmdsun
        20
    mmdsun  
       2023-05-31 12:55:40 +08:00 via iPhone
    整个算完后再 setScale 设置精度呢?
    newaccount
        21
    newaccount  
       2023-05-31 13:09:12 +08:00
    @newaccount 说的有点含糊。具体就是套数学公式,转换成只有一个除法计算。最后计算除法的时候同时设置 divide(xxx, scale, roundingMode),这样可以获得最接近结果。另外对于可能有小数的运算(比如乘 0.3 ),提前放大成整数运算,最后再除回去。当成蝴蝶效应好了,中间的精度损失会导致最后结果越偏越多
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   979 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 21:58 · PVG 05:58 · LAX 13:58 · JFK 16:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.