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

在处理现实世界的货币价值的时候,我build议使用BigDecimal而不是Double。但是我没有一个令人信服的解释,只是“通常这样做”。

你可以谈谈这个问题吗?

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

(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 float:

0.60833335
0.5416667
0.4166667

如果你是一家银行,每天都要负责数以千计的交易,即使他们不是来自同一个账户(也可能是他们),你必须有可靠的数字。 二进制浮点数是不可靠的 – 除非你明白它们是如何工作的,它们的局限性。

我认为这个描述了你的问题的解决scheme: Java陷阱:大十进制和双倍的问题在这里

从原来的博客,现在似乎下来。

Java陷阱:双

当学徒程序员走在软件开发的道路上时,许多陷阱都摆在他面前。 本文通过一系列实例说明了使用Java的简单typesdouble和float的主要陷阱。 但是请注意,为了完全掌握数值计算的准确性,您需要一个关于该主题的教科书(或两个)。 因此,我们只能抓住话题的表面。 这就是说,这里所传达的知识应该给你的基本知识,以发现或识别代码中的错误。 这是我认为任何专业软件开发人员应该知道的知识。

  1. 十进制数字是近似值

    虽然0到255之间的所有自然数都可以用8位精确描述,但描述0.0到255.0之间的所有实数都需要无限的位数。 首先,在这个范围内(即使在0.0 – 0.1的范围内)也有无穷多的数字来描述,其次,某些无理数根本不能用数字来描述。 例如e和π。 换句话说,数字2和0.2在计算机中的表示方式大不相同。

    整数由代表值2n的位表示,其中n是位的位置。 因此,值6被表示为对应于比特序列0110的23 * 0 + 22 * 1 + 21 * 1 + 20 * 0另一方面,小数由表示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 

    1.0被打印。 虽然这确实是正确的,但它可能会给我们一种错误的安全感。 巧合的是,0.2是Java能够正确表示的less数几个值之一。 让我们再次挑战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 

    根据0.100000001490116119384765625博客的幻灯片,这两个计算的0.100000001490116119384765625分别是0.1000000014901161193847656250.1000000000000000055511151231... 这些结果对于有限的一组数字是正确的。 float的精度为8位,而double的精度为17位。 现在,如果预期的结果1.0和屏幕上显示的结果之间的概念不匹配不足以让你的警钟响起,那么请注意来自先生的数字。 达西的幻灯片似乎不符合印刷的数字! 那是另一个陷阱。 更进一步下去。

    通过看似简单的可能情景,我们已经意识到错误的计算,所以我们有必要考虑这种印象有多快可以起作用。让我们简化问题,只添加三个数字。

     System.out.println( 0.3 == 0.1d + 0.1d + 0.1d ); false 

    令人震惊的是,这种不准确的情况已经在三次增加了!

  2. 双打溢出

    和Java中的其他任何简单types一样,double也是由一组有限的位来表示的。 因此,增加一个值或乘以一个double可以产生令人惊讶的结果。 诚然,为了溢出,数字必须相当大,但是却发生了。 让我们尝试乘以然后划分一个大数字。 math直觉说结果是原始数字。 在Java中,我们可能会得到不同的结果。

     double big = 1.0e307 * 2000 / 2000; System.out.println( big == 1.0e307 ); false 

    这里的问题是大的先乘,溢出,然后溢出的数字被分开。 更糟糕的是,程序员没有发现任何exception或其他types的警告。 基本上,这使得expression式x * y完全不可靠,因为在一般情况下对于由x,y表示的所有double值没有指示或保证。

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

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

     System.out.println( 1234.0d + 1.0e-13d == 1234.0d ); true 

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

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

    到目前为止,我们所学到的是,我们必须抛弃我们在math课程和整数规划中获得的所有直觉。 谨慎使用小数。 例如, for(double d = 0.1; d != 0.3; d += 0.1)的声明实际上是一个伪装的永无止境的循环! 错误的是直接比较十进制数字。 您应该遵守以下指导原则。

    避免两个十进制数之间的平等testing。 if(a == b) {..} ,请使用if(Math.abs(ab) < tolerance) {..}其中容忍可以是一个常量定义为例如public static final double tolerance = 0.01考虑作为替代使用操作符<,>,因为它们可以更自然地描述你想要expression的内容。 例如, for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) ,我更喜欢for(double d = 0; d <= 10.0; d+= 0.1)虽然forms各有其优点,但是:unit testing时,我更愿意expressionassertEquals(2.5, d, tolerance) over assertTrue(d > 2.5)不仅第一种forms读得更好,常常是检查你想要做的事(即那个d不是太大)。

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

    所见即所得通常用于graphics用户界面应用程序中。 这意味着“你看到的就是你得到的东西”,并用于计算来描述一个系统,其中在编辑过程中显示的内容看起来与最终输出非常相似,可能是打印的文档,网页等。短语最初是Flip Wilson的拖拉人物“杰拉尔丁”(Geraldine)的一个stream行口头禅,他常常会说“你看到的就是你得到的东西”,原谅她古怪的行为(来自维基百科)。

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

    然而,我们可以通过一些巧妙的手段来调查幕后的真实情况。 现在我们知道数字0.1用一些近似表示。

     System.out.println( 0.1d ); 0.1 

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

    为了多样化,我们再选一个无辜的数字,比如2.3。 像0.1一样,2.3是一个近似值。 不出所料,当打印数字Java隐藏的近似。

     System.out.println( 2.3d ); 2.3 

    为了研究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 

    所以2.2999999999999997就是2.3的2.3那么多! 另外请注意,由于近似值,枢轴点是在.999997,而不是.999995,你通常在math上舍入。 另一种处理近似值的方法是调用BigDecimal的服务。

     System.out.println( new BigDecimal(2.3d) ); 2.29999999999999982236431605997495353221893310546875 

    现在,不要以为你可以跳船而只使用BigDecimal就可以成功。 BigDecimal在这里logging了自己的陷阱集合。

    没有什么是容易的,很less有任何东西是免费的。 而“自然地”,浮动和双打在印刷/书写时会产生不同的结果。

     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 

    根据Joseph D. Darcy博客的幻灯片,float近似有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 

    用负数除正数得到负值,用负数除负数得到正值。 由于除以零是可能的,你将得到不同的结果,取决于你是否用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 
  7. 无限是奇怪的

    在math的世界里,无穷是一个我很难理解的概念。 例如,当一个无限大于另一个时,我从来没有获得过直觉。 当然Z> N,所有有理数的集合比自然数的集合要大得多,但这在我看来就是这个直觉的极限!

    幸运的是,Java中的无穷大与math世界中的无穷大是不可预测的。 你可以执行通常的犯罪嫌疑人(+, – ,*,/无限值,但是你无法将无穷大应用于无穷大。

     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 

    这里的主要问题是返回的NaN值没有任何警告。 因此,如果你愚蠢地调查一个特定的双数是偶数还是多数,那么你真的会陷入一个多毛的境地。 也许运行时exception会更合适?

     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 

    突然,你的variables既不奇怪也不均匀! 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 

    一般来说,当一个double获得NaN的值时,任何操作都会导致NaN。

     System.out.println( nan + 1.0 ); NaN 
  8. 结论

    1. 十进制数字是近似值,而不是您分配的值。 在math世界中获得的任何直觉不再适用。 期望a+b = aa != a/3 + a/3 + a/3
    2. 避免使用==,比较一些容差或使用> =或<=运算符
    3. Java是WYSINWYG! 不要相信打印/写入的值是近似值,因此总是以相同的格式读/写十进制数。
    4. 注意不要让你的双倍溢出,不要让你的双倍进入±Infinity或NaN的状态。 在任何一种情况下,您的计算可能都不会像您预期的那样结束。 您可能会发现在您的方法中返回值之前总是检查这些值是一个好主意。

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

一个常见的例子就是金钱。 尽pipe在99%的用例中,金额不会太大以至于不需要BigDecimal的精确度,但是使用BigDecimal常常被认为是最佳实践,因为四舍五入的控制在软件中,避免了开发人员处理四舍五入错误。 即使你有信心,你也可以用double来处理舍入,我build议你使用helper方法来执行彻底testing的舍入。

这主要是出于精度的原因。 BigDecimal存储无限精度的浮点数。 你可以看看这个解释得很好的页面。 http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal

当使用BigDecimal时,它可以存储更多的数据,然后Double,这使得它更加准确,而且只是现实世界中更好的select。

虽然它慢得多,但它是值得的。

打赌你不想给你的老板不准确的信息,是吧?

另一个想法:跟踪long的美分数量。 这更简单,避免BigDecimal的繁琐语法和性能下降。

精确的财务计算是非常重要的,因为人们因为四舍五入的错误而消失时非常愤怒,这就是为什么double是处理金钱的可怕select。