我可以看到在处理货币计算时可以搞砸你的四种基本方法。double
曼蒂萨太小
尾数中精度为约15位十进制数字,因此每次处理大于该值的金额时,您都会得到错误的结果。如果你正在跟踪美分,问题将在1013(十万亿)美元之前开始发生。
虽然这是一个很大的数字,但并不是那么大。美国GDP约为18万亿,因此任何涉及国家甚至公司规模金额的事情都很容易得到错误的答案。
此外,在计算过程中,有很多方法可以使较小的金额超过此阈值。您可能正在执行增长预测或多年,这会产生较大的最终值。您可能正在执行“假设”方案分析,其中检查了各种可能的参数,并且参数的某些组合可能会导致非常大的值。你可能在财务规则下工作,允许一美分的一小部分,这可能会从你的范围中再削减两个数量级或更多,使你大致符合美元中个人财富。
最后,我们不要以美国为中心的观点。其他货币呢?一美元的价值大约相当于13,000印尼盾,所以这是另外2个数量级,你需要跟踪该货币的货币金额(假设没有“美分”!你几乎已经降到了凡人感兴趣的金额。
下面是一个示例,其中从 1e9 开始的 5% 的增长预测计算出错:
method year amount delta
double 0 $ 1,000,000,000.00
Decimal 0 $ 1,000,000,000.00 (0.0000000000)
double 10 $ 1,628,894,626.78
Decimal 10 $ 1,628,894,626.78 (0.0000004768)
double 20 $ 2,653,297,705.14
Decimal 20 $ 2,653,297,705.14 (0.0000023842)
double 30 $ 4,321,942,375.15
Decimal 30 $ 4,321,942,375.15 (0.0000057220)
double 40 $ 7,039,988,712.12
Decimal 40 $ 7,039,988,712.12 (0.0000123978)
double 50 $ 11,467,399,785.75
Decimal 50 $ 11,467,399,785.75 (0.0000247955)
double 60 $ 18,679,185,894.12
Decimal 60 $ 18,679,185,894.12 (0.0000534058)
double 70 $ 30,426,425,535.51
Decimal 70 $ 30,426,425,535.51 (0.0000915527)
double 80 $ 49,561,441,066.84
Decimal 80 $ 49,561,441,066.84 (0.0001678467)
double 90 $ 80,730,365,049.13
Decimal 90 $ 80,730,365,049.13 (0.0003051758)
double 100 $ 131,501,257,846.30
Decimal 100 $ 131,501,257,846.30 (0.0005645752)
double 110 $ 214,201,692,320.32
Decimal 110 $ 214,201,692,320.32 (0.0010375977)
double 120 $ 348,911,985,667.20
Decimal 120 $ 348,911,985,667.20 (0.0017700195)
double 130 $ 568,340,858,671.56
Decimal 130 $ 568,340,858,671.55 (0.0030517578)
double 140 $ 925,767,370,868.17
Decimal 140 $ 925,767,370,868.17 (0.0053710938)
double 150 $ 1,507,977,496,053.05
Decimal 150 $ 1,507,977,496,053.04 (0.0097656250)
double 160 $ 2,456,336,440,622.11
Decimal 160 $ 2,456,336,440,622.10 (0.0166015625)
double 170 $ 4,001,113,229,686.99
Decimal 170 $ 4,001,113,229,686.96 (0.0288085938)
double 180 $ 6,517,391,840,965.27
Decimal 180 $ 6,517,391,840,965.22 (0.0498046875)
double 190 $ 10,616,144,550,351.47
Decimal 190 $ 10,616,144,550,351.38 (0.0859375000)
三角洲(与第一次之间的差异在160年达到1美分>,大约2万亿(可能不是160年后的全部),当然只会变得更糟。double
BigDecimal
当然,53位的Mantissa意味着这种计算的相对误差可能非常小(希望你不会失去2万亿分之一的工作)。事实上,相对误差在大部分示例中基本上保持相当稳定。您当然可以组织它,以便(例如)减去两个变量,并在尾数中损失精度,从而导致任意大的错误(练习到读者)。
更改语义
因此,您认为自己非常聪明,并设法提出了一个舍入方案,允许您在本地JVM上使用并详尽地测试您的方法。继续部署它。明天或下周,或者任何时候对你来说最糟糕的时候,结果都会改变,你的伎俩就会失败。double
与几乎所有其他基本语言表达式不同,当然也与整数或算术不同,默认情况下,由于 strictfp 功能,许多浮点表达式的结果没有单个标准定义值。平台可以自行决定免费使用更高精度的中间体,这可能会导致在不同的硬件,JVM版本等上产生不同的结果。对于相同的输入,当方法从解释型切换到JIT编译时,结果甚至可能在运行时有所不同!BigDecimal
如果你在Java之前的1.2天里编写了你的代码,那么当Java 1.2突然引入现在默认的变量FP行为时,你会非常生气。你可能会想在任何地方使用,并希望你不会遇到任何相关的错误 - 但在某些平台上,你会抛弃许多性能,而这些性能本来就给你带来了双重好处。strictfp
没有什么可以说JVM规范将来不会再次更改以适应FP硬件的进一步变化,或者JVM实现者不会使用默认的非严格fp行为给他们的绳索来做一些棘手的事情。
不准确的表示
正如Roland在他的答案中指出的那样,一个关键问题是它对一些大多数非整数值没有精确的表示。虽然在某些情况下(例如,)一个非精确值通常会“往返”OK,但一旦你对这些不精确的值进行数学计算,误差就会加剧,这可能是不可恢复的。double
0.1
Double.toString(0.1).equals("0.1")
特别是,如果您“接近”一个舍入点,例如~1.005,您可能会得到1.00499999...当真实值为 1.0050000001...时,反之亦然。由于错误是双向的,因此没有四舍五入的魔法可以解决此问题。没有办法判断值 1.004999999...是否应该颠簸。你的方法(一种双舍入)之所以有效,是因为它处理了1.0049999应该被颠簸的情况,但它永远无法越过边界,例如,如果累积错误导致1.005000000000001变成1.004999999999999,它无法修复它。roundToTwoPlaces()
你不需要大或小的数字来达到这个目标。您只需要一些数学运算,结果就可以接近边界。你做的数学运算越多,与真实结果的可能偏差就越大,跨越边界的机会就越大。
按照此处的要求,执行简单计算的搜索测试:并将其四舍五入到小数点后2位(即美元和美分)。那里有一些舍入方法,目前使用的一种是你的1的增强版本(通过增加第一个舍入的乘数,你使它更加敏感 - 原始版本在琐碎的输入上立即失败)。amount * tax
roundToTwoPlacesB
n
测试会吐出它发现的失败,它们成群结队地出现。例如,前几个失败:
Failed for 1234.57 * 0.5000 = 617.28 vs 617.29
Raw result : 617.2850000000000000000000, Double.toString(): 617.29
Failed for 1234.61 * 0.5000 = 617.30 vs 617.31
Raw result : 617.3050000000000000000000, Double.toString(): 617.31
Failed for 1234.65 * 0.5000 = 617.32 vs 617.33
Raw result : 617.3250000000000000000000, Double.toString(): 617.33
Failed for 1234.69 * 0.5000 = 617.34 vs 617.35
Raw result : 617.3450000000000000000000, Double.toString(): 617.35
请注意,“原始结果”(即确切的未舍入结果)始终接近边界。您的舍入方法在高边和低边都是错误的。您无法一般地修复它。x.xx5000
不精确的计算
一些方法不需要正确舍入的结果,而是允许最多2.5 ulp的错误。当然,您可能不会在货币中使用过多的双曲函数,但是诸如和之类的函数经常会进入货币计算,而这些函数的准确度仅为1 ulp。因此,当返回时,该数字已经是“错误的”。java.lang.Math
exp()
pow()
这与“不精确表示”问题相互作用,因为这种类型的错误比正常的数学运算中的错误严重得多,这些运算至少从可表示域中选择最佳值。这意味着,当您使用这些方法时,您可以拥有更多的圆形边界交叉事件。double