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

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

  •  
  •   Gct012 · 322 天前 · 2712 次点击
    这是一个创建于 322 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在 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  
       322 天前
    java 这个才是正常现象好么一点也不神奇,不如说 Oracle 数据库或者是 windows 自带的计算器中,使用 0.33333333333333333 * 6 ,得到的结果均是 2 才是异常的。盲猜一下不一定准是计算器缓存了前面的除法表达式,然后如果后面有乘法就先做乘法运算,再做除法运算
    cc666
        2
    cc666  
       322 天前
    解:不用 bigdecimal 即可
    jshell> 0.33333333333333333 * 6
    $2 ==> 2.0
    shanch
        3
    shanch  
       322 天前
    你用其他计算器先计算 1 / 3 等于的值 * 6 试试
    mineralsalt
        4
    mineralsalt  
       322 天前
    我试了一下, java 也是 2, 不知道你是怎么计算的

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

    结果: 2.0
    cc666
        5
    cc666  
       322 天前
    PS1 ,这没啥神奇的,基本的计算机组成原理
    PS2 ,你问题中,1 / 3 * 6 和 0.33333333333333333 * 6 根本不是等价的
    PS3 ,经验证,windows 自带的计算器,0.33333333333333333 * 6 = 1.99999999999999998 ,win11 22H2
    Gct012
        6
    Gct012  
    OP
       322 天前
    @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  
       322 天前
    换 float 运算 1.0/3*6 结果都是 2

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

    而是用 bigdecimal 是精确计算,1/3 是无限循环会要设置精度,所以都是精确计算。
    你要做到一样就把结果转成 double 吧……
    urnoob
        15
    urnoob  
       322 天前 via Android
    @mineralsalt
    您这写不对啊。。。在变 bigdxxxx 前就已经算好了结果。。。
    qwerthhusn
        16
    qwerthhusn  
       322 天前   ❤️ 1
    看了这么多评论,我感觉好像我也没那么菜,找工作的信心又增加了一分。
    luhe
        17
    luhe  
       322 天前 via iPhone
    业务要求先算除法保留小数位,即使丢失精度也在所不惜么,这个小数位给你提需求具体是多少了么
    nothingistrue
        18
    nothingistrue  
       321 天前   ❤️ 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  
       321 天前
    调整计算顺序,先计算乘法。需要设置 setScale 的放到最后计算,只需要保证数学上相等就行,精度问题无解
    mmdsun
        20
    mmdsun  
       321 天前 via iPhone
    整个算完后再 setScale 设置精度呢?
    newaccount
        21
    newaccount  
       321 天前
    @newaccount 说的有点含糊。具体就是套数学公式,转换成只有一个除法计算。最后计算除法的时候同时设置 divide(xxx, scale, roundingMode),这样可以获得最接近结果。另外对于可能有小数的运算(比如乘 0.3 ),提前放大成整数运算,最后再除回去。当成蝴蝶效应好了,中间的精度损失会导致最后结果越偏越多
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3324 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 13:06 · PVG 21:06 · LAX 06:06 · JFK 09:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.