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

|                                                                                                                   这个问题已经在这里有了答案:                                                      
已邀请:
        这就是所谓的精度损失,在处理非常大的数字或非常小的数字时非常明显。在许多情况下,带小数的十进制数字的二进制表示形式是近似值,而不是绝对值。要了解为什么需要阅读二进制形式的浮点数表示形式。这是一个链接: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双倍: 0.6083333333333333 0.5416666666666666 0.41666666666666663 Java浮点数: 0.60833335 0.5416667 0.4166667 如果您是一家银行,并且每天要处理数千笔交易,即使它们并非往返于同一帐户(或也许是同一帐户),您也必须拥有可靠的号码。二进制浮点数不可靠-除非您了解它们的工作方式及其局限性,否则不可靠。     
我认为这描述了您的问题的解决方案:Java陷阱:大十进制以及此处出现双精度问题 来自原始博客,该博客现在似乎已关闭。 Java陷阱:双倍 学徒程序员在走软件开发道路时面临许多陷阱。本文通过一系列实际示例说明了使用Java的简单类型double和float的主要陷阱。但是请注意,要在数值计算中完全包含精度,您需要一本(或两本)关于该主题的教科书。因此,我们只能从头开始讨论话题。话虽如此,此处传达的知识应为您提供发现或识别代码中的错误所需的基本知识。我认为这是任何专业软件开发人员都应该了解的知识。 小数是近似值 虽然可以使用8位精确地描述0至255之间的所有自然数,但是描述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的位描述,即分数ѭ1number。数字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能够正确表示的少数几个值之一。让我们通过另一个琐碎的算术问题再次挑战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
根据约瑟夫·D·达西博客中的幻灯片,两个计算的总和分别为
0.100000001490116119384765625
0.1000000000000000055511151231...
。这些结果对于有限的一组数字是正确的。浮点数的精度为8个首位数,而double数的精度为17个首位数。现在,如果预期结果1.0与屏幕上打印的结果之间的概念上的不匹配不足以使您的警钟响起,请注意Mr.的数字。达西的幻灯片似乎与印刷的数字不符!那是另一个陷阱。关于此的更多信息。 在看似最简单的情况下意识到了错误的计算之后,就可以合理地考虑印象可能会出现多快了。让我们将问题简化为仅添加三个数字。
System.out.println( 0.3 == 0.1d + 0.1d + 0.1d );
false
令人震惊的是,不精确性已经在三个附加项中开始发挥作用! 加倍溢出 与Java中的任何其他简单类型一样,双精度数由一组有限的位表示。因此,将一个值加或乘以一个双可以产生令人惊讶的结果。诚然,数字必须很大才能溢出,但是它确实发生了。让我们尝试相乘然后除以一个大数。数学直觉说结果是原始数。在Java中,我们可能会得到不同的结果。
double big = 1.0e307 * 2000 / 2000;
System.out.println( big == 1.0e307 );
false
这里的问题是先将big乘以并溢出,然后再将溢出数除。更糟糕的是,不会向程序员发出异常或其他类型的警告。基本上,这使表达式x * y完全不可靠,因为在通常情况下,对于由x,y表示的所有double值均不作任何表示或保证。 大大小小的不是朋友! 劳雷尔和哈代经常在很多事情上意见分歧。同样在计算中,大大小小的都不是朋友。使用固定位数表示数字的结果是,在相同的计算中对非常大和非常小的数字进行运算将无法按预期进行。让我们尝试将小的内容添加到大的内容中。
System.out.println( 1234.0d + 1.0e-13d == 1234.0d );
true
加法没有效果!这与任何(理智的)加法直觉相矛盾,后者表示给定两个数字d和f为正数,则d + f> d。 小数不能直接比较 到目前为止,我们学到的是,我们必须抛弃在数学课和整数编程中获得的所有直觉。请谨慎使用小数。例如,语句“ 11”实际上是伪装的永无止境的循环!错误是直接将十进制数字相互比较。您应遵循以下准则。 避免两个小数之间的相等性测试。请勿使用
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不太大)。 所见即所得-所见即所得 所见即所得是通常在图形用户界面应用程序中使用的表达式。它的意思是“您所看到的就是您所得到的”,用于计算以描述一个系统,在该系统中,编辑期间显示的内容看起来与最终输出非常相似,该最终输出可能是打印的文档,网页等。该短语最初是一个流行的流行短语,由Flip Wilson的扮靓人物“ Geraldine”提出,他常常说“您看到的就是所得到的东西”来原谅她的古怪行为(来自维基百科)。 程序员经常遇到的另一个严重陷阱是,他们认为十进制数是所见即所得。必须意识到,在打印或写入十进制数时,不是要打印/写入的近似值。换一种说法,Java在幕后做了很多近似,并且不断地试图使您不了解它。只有一个问题。您需要了解这些近似值,否则您的代码中可能会遇到各种各样的神秘错误。 但是,只要有一些独创性,我们就可以调查幕后的真实情况。到现在为止,我们知道数字0.1已被近似表示。
System.out.println( 0.1d );
0.1
我们知道0.1不是0.1,但屏幕上仍打印了0.1。结论:Java是所见即所得! 为了多样化,让我们选择另一个无辜的数字,例如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!还要注意,由于近似值,枢轴点位于..99997处,而不是..99995处,通常在数学上四舍五入。掌握近似值的另一种方法是调用BigDecimal的服务。
System.out.println( new BigDecimal(2.3d) );
2.29999999999999982236431605997495353221893310546875
现在,不要为自己的桂冠而休息,以为您可以跳船,只使用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
根据约瑟夫·D·达西(Joseph D. Darcy)博客的幻灯片,浮点近似具有24个有效位,而双近似具有53个有效位。士气是为了保留值,必须以相同格式读取和写入十进制数字。 被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
无限怪异 在数学世界中,无穷大是我很难理解的概念。例如,当一个无穷大比另一个无穷大时,我从没有获得直觉。当然,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
这里的主要问题是返回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
突然,您的变量既不是奇数也不是偶数! NaN比Infinity更奇怪 无限值与double的最大值不同,并且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
通常,当双精度数已获取值NaN时,对其进行任何运算都会得到NaN。
System.out.println( nan + 1.0 );
NaN
结论 小数是近似值,而不是您分配的值。在数学世界中获得的任何直觉都不再适用。期待
a+b = a
a != a/3 + a/3 + a/3
避免使用==,与一些容差进行比较,或者使用> =或<=运算符 Java是所见即所得!永远不要相信您打印/写入的值是近似值,因此始终以相同格式读取/写入十进制数。 请注意不要溢出双精度数,不要使双精度数变为±Infinity或NaN。无论哪种情况,您的计算结果都可能不符合您的预期。您可能会发现,最好在返回方法值之前始终检查这些值。     
尽管BigDecimal可以存储比double精度高的精度,但是通常不需要这样做。使用它的真正原因是因为它可以清楚地说明如何进行舍入,包括许多不同的舍入策略。在大多数情况下,您可以用两倍来获得相同的结果,但是除非您知道所需的技术,否则BigDecimal是解决这些问题的方法。 一个常见的例子是金钱。即使资金不足以在99%的用例中需要BigDecimal的精度,但通常仍被认为是使用BigDecimal的最佳实践,因为舍入控制在软件中,这避免了开发人员可能做出的风险。处理舍入时出错。即使您有信心可以用ѭ31进行舍入,我还是建议您使用辅助方法来进行彻底测试的舍入。     
        这样做主要是出于精度考虑。 BigDecimal以无限的精度存储浮点数。您可以看一下对此页面进行了很好解释的页面。 http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal     
        使用BigDecimal时,与Double相比,它可以存储更多的数据,这使它更准确,并且是现实世界中更好的选择。 尽管它变慢和变长了很多,但这是值得的。 打赌,您不想给您的老板不正确的信息,是吗?     
        另一个想法:跟踪32英镑中的美分数。这更简单,并且避免了繁琐的语法和ѭ33的性能降低。 财务计算的精确性尤为重要,因为当四舍五入的错误导致人们的钱财消失时,人们会非常生气,这就是为什么
double
是处理钱财的糟糕选择。     

要回复问题请先登录注册