Java:为什么我们应该在现实世界中使用BigDecimal而不是Double?

Vin*_*C M 51 java floating-point double bigdecimal

在处理现实世界的货币价值时,我建议使用BigDecimal而不是Double.But我没有一个令人信服的解释,除了"通常是这样做".

你能否对这个问题嗤之以鼻?

Яро*_*лин 38

它被称为精度损失,并且在使用非常大的数字或非常小的数字时非常明显.具有基数的十进制数的二进制表示在许多情况下是近似值而不是绝对值.要理解为什么需要读取二进制中的浮点数表示.这是一个链接:http://en.wikipedia.org/wiki/IEEE_754-2008.这是一个快速演示:
bc(任意精度计算器语言),精度= 10:

(1/3 + 1/12 + 1/8 + 1/30)= 0.6083333332
(1/3 + 1/12 + 1/8)= 0.541666666666666
(1/3 + 1/12)= 0.416666666666666

Java double:
0.6083333333333333
0.5416666666666666
0.41666666666666663

Java浮动:

0.60833335
0.5416667
0.4166667


如果您是银行并且每天负责数千笔交易,即使他们不是来自同一个帐户(或者可能是他们),您也必须拥有可靠的号码.二进制花车不可靠 - 除非您了解它们的工作原理和限制.

  • @vinoth ...并且正如你所说"但我没有一个令人信服的解释,除了,"它通常以这种方式完成"",你应该与你的同事分享这个页面:-) (2认同)
  • 因此,银行必须将涉及非理性数字(如 1/3)的 10 万亿笔交易相加才能有所作为。当然,每笔交易(或每千亿美元)都会删除除最后 1/100 美分之外的所有小数位以纠正这个问题——如果存在的话,但我从未听说过银行使用 1/3。不过,杂货店可能会以 1 美元的价格出售 3 美元。Peter Lawrey 在下面给出了真正的答案——BigDecimal 有四舍五入的方法,可以防止开发人员出错。听起来这里的很多人并没有比“通常是那样做的”更有把握 (2认同)

zen*_*ngr 34

我觉得这说明解决问题的方法:大十进制:Java的陷阱双,问题在这里

从原来的博客看来现在已经下来了.

Java陷阱:加倍

当他走在软件开发的道路上时,许多陷阱摆在学徒程序员面前.本文通过一系列实际例子说明了使用Java的简单类型double和float的主要陷阱.但是,请注意,要完全接受数值计算的精度,您需要有关于该主题的教科书(或两本).因此,我们只能抓住主题的表面.话虽如此,这里传达的知识应该为您提供发现或识别代码中的错误所需的基本知识.我认为任何专业软件开发人员都应该了解这些知识.

  1. 十进制数是近似值

    虽然可以使用8位精确地描述0到255之间的所有自然数,但是描述0.0到255.0之间的所有实数需要无限数量的位.首先,在该范围内存在无限多个数字(即使在0.0-0.1的范围内),其次,某些无理数在数值上根本无法描述.例如e和π.换句话说,数字2和0.2在计算机中有很大的不同.

    整数由表示值2n的位表示,其中n是位的位置.因此,值6被表示为23 * 0 + 22 * 1 + 21 * 1 + 20 * 0对应于比特序列0110.另一方面,小数由表示2-n的比特来描述,即分数1/2, 1/4, 1/8,....数字0.75对应于2-1 * 1 + 2-2 * 1 + 2-3 * 0 + 2-4 * 0产生比特序列1100 (1/2 + 1/4).

    有了这些知识,我们可以制定以下经验法则:任何十进制数都用近似值表示.

    让我们通过执行一系列简单的乘法来研究这种实际后果.

    System.out.println( 0.2 + 0.2 + 0.2 + 0.2 + 0.2 );
    1.0
    
    Run Code Online (Sandbox Code Playgroud)

    打印1.0.虽然这确实是正确的,但它可能会给我们一种虚假的安全感.巧合的是,0.2是Java能够正确表示的少数几个值之一.让我们用另一个微不足道的算术问题再次挑战Java,将数量增加0.1倍.

    System.out.println( 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f );
    System.out.println( 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d );
    
    1.0000001
    0.9999999999999999
    
    Run Code Online (Sandbox Code Playgroud)

    据约瑟夫D.达西博客幻灯片两个计算的总和是0.1000000014901161193847656250.1000000000000000055511151231...分别.这些结果对于一组有限的数字是正确的.float的精度为8位前导数字,而double的精度为17位.现在,如果预期结果1.0与屏幕上打印的结果之间的概念不匹配不足以让你的警钟响起,那么请注意先生的数字如何.Darcy的幻灯片似乎与打印的数字不对应!那是另一个陷阱.更多关于此进一步下来.

    在看似简单的可能情景中已经意识到错误的计算,考虑印象可能有多快进入是合理的.让我们简化问题,只添加三个数字.

    System.out.println( 0.3 == 0.1d + 0.1d + 0.1d );
    false
    
    Run Code Online (Sandbox Code Playgroud)

    令人震惊的是,这种不精确已经在三次加成中开始了!

  2. 双打溢出

    与Java中的任何其他简单类型一样,double由一组有限位表示.因此,添加值或乘以double可以产生令人惊讶的结果.不可否认,为了溢出,数字必须非常大,但它会发生.让我们尝试乘以然后除以一个大数.数学直觉说结果是原始数字.在Java中,我们可能得到不同的结果.

    double big = 1.0e307 * 2000 / 2000;
    System.out.println( big == 1.0e307 );
    false
    
    Run Code Online (Sandbox Code Playgroud)

    这里的问题是大的第一次乘以溢出,然后溢出的数字被分割.更糟糕的是,程序员不会发出异常或其他类型的警告.基本上,这使得表达式x*y完全不可靠,因为对于由x,y表示的所有双值,在一般情况下没有指示或保证.

  3. 大大小小都不是朋友!

    劳雷尔和哈代经常对很多事情持不同意见.同样在计算中,大小都不是朋友.使用固定数量的位来表示数字的结果是,在相同的计算中操作非常大且非常小的数字将无法按预期工作.让我们尝试添加一些小东西.

    System.out.println( 1234.0d + 1.0e-13d == 1234.0d );
    true
    
    Run Code Online (Sandbox Code Playgroud)

    添加没有效果!这与任何(理智的)加法的数学直觉相矛盾,这说明给定两个数字正数d和f,则d + f> d.

  4. 十进制数不能直接比较

    到目前为止我们学到的是,我们必须抛弃我们在数学课上获得的所有直觉和用整数编程.谨慎使用十进制数.例如,该语句for(double d = 0.1; d != 0.3; d += 0.1)实际上是一个伪装的永无止境的循环!错误是将十进制数直接相互比较.您应该遵守以下指南.

    避免两个十进制数之间的相等测试.避免if(a == b) {..},使用if(Math.abs(a-b) < tolerance) {..}容差可以是常量定义为例如公共静态最终双容差= 0.01考虑作为使用运算符<,>的替代方案,因为它们可以更自然地描述您想要表达的内容.例如,我更喜欢形式 for(double d = 0; d <= 10.0; d+= 0.1)而不是更笨拙 for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) 两种形式都有其优点,具体取决于具体情况:当单元测试时,我更喜欢表示不仅assertEquals(2.5, d, tolerance)assertTrue(d > 2.5)第一种形式不仅更好,它通常是你想要的检查做(即d不是太大).

  5. WYSINWYG - 你看到的不是你得到的

    WYSIWYG是一种通常用于图形用户界面应用程序的表达式.这意味着,"所见即所得",并用于计算,以描述在编辑过程中显示的内容与最终输出非常相似的系统,最终输出可能是打印文档,网页等.短语最初是一个流行的口号,由Flip Wilson的拖曳角色"Geraldine"发起,他常常会说"你看到的就是你所得到的",原谅她古怪的行为(来自维基百科).

    另一个严重的陷阱程序员常常陷入其中,认为十进制数字是WYSIWYG.必须认识到,当打印或写入十进制数时,它不是打印/写入的近似值.换句话说,Java在幕后做了很多近似,并且一直试图保护你不要知道它.只有一个问题.您需要了解这些近似值,否则您可能会遇到代码中的各种神秘错误.

    但是,通过一些独创性,我们可以调查幕后真实情况.到目前为止,我们知道数字0.1用一些近似值表示.

    System.out.println( 0.1d );
    0.1
    
    Run Code Online (Sandbox Code Playgroud)

    我们知道0.1不是0.1,而是在屏幕上打印0.1.结论:Java是WYSINWYG!

    为了多样性,让我们选择另一个无辜的数字,比如说2.3.与0.1一样,2.3是近似值.毫不奇怪,当打印数字时,Java会隐藏近似值.

    System.out.println( 2.3d );
    2.3
    
    Run Code Online (Sandbox Code Playgroud)

    为了研究2.3的内部近似值,我们可以将数字与近距离的其他数字进行比较.

    double d1 = 2.2999999999999996d;
    double d2 = 2.2999999999999997d;
    System.out.println( d1 + " " + (2.3d == d1) );
    System.out.println( d2 + " " + (2.3d == d2) );
    2.2999999999999994 false
    2.3 true
    
    Run Code Online (Sandbox Code Playgroud)

    所以2.2999999999999997和2.3一样多2.3!另请注意,由于近似值,旋转点位于..99997而不是..99995,您通常会在数学中向上舍入.掌握近似值的另一种方法是调用BigDecimal的服务.

    System.out.println( new BigDecimal(2.3d) );
    2.29999999999999982236431605997495353221893310546875
    
    Run Code Online (Sandbox Code Playgroud)

    现在,不要停留在你的桂冠上,以为你只能跳船而只能使用BigDecimal.BigDecimal有自己的陷阱集合,在这里记录.

    没有什么是容易的,很少有任何东西是免费的.并且"自然地",浮动和双打在打印/书写时产生不同的结果.

    System.out.println( Float.toString(0.1f) );
    System.out.println( Double.toString(0.1f) );
    System.out.println( Double.toString(0.1d) );
    0.1
    0.10000000149011612
    0.1
    
    Run Code Online (Sandbox Code Playgroud)

    根据Joseph D. Darcy博客的幻灯片,浮点近似有24个有效位,而双近似有53个有效位.士气是为了保存值,您必须以相同的格式读取和写入十进制数字.

  6. 除以0

    许多开发人员从经验中知道,将数字除以零会导致其应用程序突然终止.在int上运行时发现类似的行为是Java,但非常令人惊讶的是,不是在使用double时运行.除零之外的任何数字除以零分别产生∞或-∞.将零除以零会产生特殊的NaN,即非数字值.

    System.out.println(22.0 / 0.0);
    System.out.println(-13.0 / 0.0);
    System.out.println(0.0 / 0.0);
    Infinity
    -Infinity
    NaN
    
    Run Code Online (Sandbox Code Playgroud)

    将正数除以负数会产生负结果,而将负数除以负数会产生正结果.由于可以除零,因此根据是否将数字除以0.0或-0.0,您将获得不同的结果.对,是真的!Java有负零!不要被愚弄,两个零值相等,如下所示.

    System.out.println(22.0 / 0.0);
    System.out.println(22.0 / -0.0);
    System.out.println(0.0 == -0.0);
    Infinity
    -Infinity
    true
    
    Run Code Online (Sandbox Code Playgroud)
  7. 无限是奇怪的

    在数学世界中,无限是我发现难以理解的概念.例如,当一个无穷大比另一个无限大时,我从未获得直觉.当然Z> N,所有有理数的集合都无限大于自然数集,但这就是我在这方面的直觉极限!

    幸运的是,Java中的无穷大与数学世界中的无穷大一样难以预测.你可以执行通常的嫌疑(+, - ,*,/无限值,但你不能将无穷大应用于无穷大.

    double infinity = 1.0 / 0.0;
    System.out.println(infinity + 1);
    System.out.println(infinity / 1e300);
    System.out.println(infinity / infinity);
    System.out.println(infinity - infinity);
    Infinity
    Infinity
    NaN
    NaN
    
    Run Code Online (Sandbox Code Playgroud)

    这里的主要问题是返回NaN值而没有任何警告.因此,如果你愚蠢地调查一个特定的双重是偶数还是奇数,你真的可以进入一个毛茸茸的情况.也许运行时异常会更合适?

    double d = 2.0, d2 = d - 2.0;
    System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1));
    d = d / d2;
    System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1));
    even: true odd: false
    even: false odd: false
    
    Run Code Online (Sandbox Code Playgroud)

    突然间,你的变量既不奇怪也不奇怪!NaN甚至比无限更奇怪无限值与双精度的最大值不同,并且NaN与无限值再次不同.

    double nan = 0.0 / 0.0, infinity = 1.0 / 0.0;
    System.out.println( Double.MAX_VALUE != infinity );
    System.out.println( Double.MAX_VALUE != nan );
    System.out.println( infinity         != nan );
    true
    true
    true
    
    Run Code Online (Sandbox Code Playgroud)

    通常,当double获得值NaN时,对其的任何操作都会导致NaN.

    System.out.println( nan + 1.0 );
    NaN
    
    Run Code Online (Sandbox Code Playgroud)
  8. 结论

    1. 十进制数是近似值,而不是您指定的值.在数学世界中获得的任何直觉都不再适用.期待a+b = aa != a/3 + a/3 + a/3
    2. 避免使用==,与某些容差进行比较或使用> =或<=运算符
    3. Java是WYSINWYG!永远不要相信您打印/写入的值是近似值,因此始终以相同的格式读/写十进制数.
    4. 小心不要让你的双重溢出,不要让你的双重进入±Infinity或NaN状态.在任何一种情况下,您的计算结果可能都不如您所期望的那样.您可能会发现在返回方法中的值之前始终检查这些值是个好主意.


Pet*_*rey 29

虽然BigDecimal可以存储比double更高的精度,但通常不需要这样做.它使用的真正原因是因为它清楚地表明了如何执行舍入,包括许多不同的舍入策略.在大多数情况下,您可以使用double获得相同的结果,但除非您知道所需的技术,否则BigDecimal是这种情况下的方法.

一个常见的例子就是金钱.尽管在99%的使用情况下钱不会大到需要BigDecimal的精度,但使用BigDecimal通常被认为是最佳实践,因为在软件中控制舍入是避免了开发人员制造的风险处理四舍五入的错误.即使你有信心可以处理四舍五入,double我建议你使用辅助方法来执行彻底测试的舍入.