一个实际的示例,其中使用BigDecimal货币比使用double严格要好

一个实际的示例,其中使用BigDecimal货币比使用double严格要好,第1张

一个实际的示例,其中使用BigDecimal货币比使用double严格要好

我可以看到四种基本方法,这些方法

double
可以在处理货币计算时给您带来麻烦。

尾数太小

尾数的精度约为15位十进制数字,那么您每次处理大于该数量的值时都会得到错误的结果。如果您跟踪美分,则问题将在10 13(十万亿)美元之前开始出现。

虽然这是一个很大的数字,但并不是 那么大 。美国约18万亿的GDP超过了它的总和,因此任何与国家或什至是企业规模的交易都容易得到错误的答案。

此外,还有许多方法可以使较小的数量在计算过程中超过此阈值。您可能正在进行增长预测,或者进行了多年的增长预测,因此最终价值很大。您可能正在进行“假设分析”场景分析,其中检查了各种可能的参数,并且参数的某种组合可能会导致很大的值。您可能在财务规则下工作,该规则允许一分之几的零头可能会使您的价格范围再减少两个数量级或更多,这使您大致与美元中的个人财富保持一致。

最后,让我们不要以美国为中心。那其他货币呢?一美元的价值约合
13,000印尼盾,因此您需要另外2个数量级来跟踪该货币的货币金额(假设没有“美分”!)。您几乎要降低到凡人感兴趣的金额。

这是一个示例,其中从1e9开始于5%的增长预测计算出错:

method   year   amountdeltadouble      0  $ 1,000,000,000.00Decimal     0  $ 1,000,000,000.00  (0.0000000000)double     10  $ 1,628,894,626.78Decimal    10  $ 1,628,894,626.78  (0.0000004768)double     20  $ 2,653,297,705.14Decimal    20  $ 2,653,297,705.14  (0.0000023842)double     30  $ 4,321,942,375.15Decimal    30  $ 4,321,942,375.15  (0.0000057220)double     40  $ 7,039,988,712.12Decimal    40  $ 7,039,988,712.12  (0.0000123978)double     50 $ 11,467,399,785.75Decimal    50 $ 11,467,399,785.75  (0.0000247955)double     60 $ 18,679,185,894.12Decimal    60 $ 18,679,185,894.12  (0.0000534058)double     70 $ 30,426,425,535.51Decimal    70 $ 30,426,425,535.51  (0.0000915527)double     80 $ 49,561,441,066.84Decimal    80 $ 49,561,441,066.84  (0.0001678467)double     90 $ 80,730,365,049.13Decimal    90 $ 80,730,365,049.13  (0.0003051758)double    100$ 131,501,257,846.30Decimal   100$ 131,501,257,846.30  (0.0005645752)double    110$ 214,201,692,320.32Decimal   110$ 214,201,692,320.32  (0.0010375977)double    120$ 348,911,985,667.20Decimal   120$ 348,911,985,667.20  (0.0017700195)double    130$ 568,340,858,671.56Decimal   130$ 568,340,858,671.55  (0.0030517578)double    140$ 925,767,370,868.17Decimal   140$ 925,767,370,868.17  (0.0053710938)double    150         $ 1,507,977,496,053.05Decimal   150         $ 1,507,977,496,053.04  (0.0097656250)double    160         $ 2,456,336,440,622.11Decimal   160         $ 2,456,336,440,622.10  (0.0166015625)double    170         $ 4,001,113,229,686.99Decimal   170         $ 4,001,113,229,686.96  (0.0288085938)double    180         $ 6,517,391,840,965.27Decimal   180         $ 6,517,391,840,965.22  (0.0498046875)double    190        $ 10,616,144,550,351.47Decimal   190        $ 10,616,144,550,351.38  (0.0859375000)

差值(在160年

double
BigDecimal
首次点击之间的差额> 1美分,大约2万亿美元(从现在起160年可能还不够多),当然只会越来越糟。

当然,尾数的53位意味着这种计算的 相对
误差可能很小(希望您不会输掉2万亿美元中超过1美分的工作)。实际上,在大多数示例中,相对误差基本上保持稳定。不过,您当然可以组织它,以便(例如)在尾数上损失精度的情况下减去两个变量,从而导致任意大的错误(请读者行使)。

改变语义

因此,您认为自己很聪明,并且设法提出了一种舍入方案,该方案使您可以

double
在本地JVM上使用并进行详尽的测试。继续部署它。明天或下周,或者对您而言最糟糕的时候,结果都会改变,您的把戏也将破裂。

与几乎所有其他基本语言表达式不同,当然也与整数或

BigDecimal
算术不同,默认情况下,由于strictfp功能,许多浮点表达式的结果没有单一的标准定义值。平台可以自由决定是否自由使用更高精度的中间体,这些中间体可能会在不同的硬件,JVM版本等上导致不同的结果。对于相同的输入,结果在运行时甚至可能在方法从解释型转换为JIT时在运行时有所不同。编译!

如果您是在Java 1.2之前的日子里编写代码的,那么当Java
1.2突然引入现在默认的变量FP行为时,您会非常生气。您可能很想在

strictfp
任何地方使用它,并希望您不会遇到任何相关的错误
-但是在某些平台上,您会丢弃很多性能,而这些性能本来是可以让您双倍买下的。

没什么好说的,JVM规范将来不会再改变以适应FP硬件的进一步变化,或者JVM实现者不会使用默认的non-strictfp行为使他们做一些棘手的事情。

不精确的表示

正如罗兰(Roland)在他的回答中指出的那样,一个关键问题

double
是对于某些非整数值,它没有确切的表示形式。尽管
0.1
在某些情况下(例如
Double.toString(0.1).equals("0.1")
),单个非精确值通常会“往返”,但是一旦对这些不精确值进行数学运算,错误就会加重,并且这是无法恢复的。

特别是,如果您“接近”舍入点,例如〜1.005,则当真值为1.0050000001 …时,您可能会得到1.00499999 …的值,
反之亦然 。由于错误是双向的,因此没有可解决此问题的四舍五入方法。无法判断是否应将1.004999999
…的值调高。您的

roundToTwoPlaces()
方法(一种双精度舍入)仅适用于以下情况,因为它处理了应将1.0049999增大的情况,但它永远无法越过边界,例如,如果累积错误导致1.0050000000001变成1.00499999999999,则无法修理它。

您不需要大数字或小数字即可达到目标。您只需要进行一些数学运算即可使结果接近边界。您做的数学越多,与真实结果的可能偏差越大,跨越边界的机会也就越大。

根据此处的要求,进行简单计算的搜索测试:将

amount*tax
其四舍五入到小数点后两位(即美元和美分)。那里有一些舍入方法,当前使用的
roundToTwoPlacesB
是您的1的增强版本(通过
n
在第一舍入中增加乘数,您可以使其更加敏感-
原始版本立即在微不足道的输入上失败)。

该测试吐出了​​发现的故障,并且这些故障成束出现。例如,前几个失败:

Failed for 1234.57 * 0.5000 = 617.28 vs 617.29Raw result : 617.2850000000000000000000, Double.toString(): 617.29Failed for 1234.61 * 0.5000 = 617.30 vs 617.31Raw result : 617.3050000000000000000000, Double.toString(): 617.31Failed for 1234.65 * 0.5000 = 617.32 vs 617.33Raw result : 617.3250000000000000000000, Double.toString(): 617.33Failed for 1234.69 * 0.5000 = 617.34 vs 617.35Raw result : 617.3450000000000000000000, Double.toString(): 617.35

注意,“原始结果”(即确切的未取整结果)始终接近

x.xx5000
边界。您的舍入方法在高端和低端都有错误。您无法以一般方式修复它。

不精确的计算

几种

java.lang.Math
方法不需要正确的取整结果,而是允许最大2.5ulp的误差。当然,您可能不会对货币使用过多的双曲线函数,但是诸如
exp()
和这样的函数
pow()
经常会在货币计算中找到用,它们的精度仅为1ulp。因此,返回数字时该数字已经“错误”。

这与“不精确表示”问题相关,因为这种类型的错误比正常的数学运算要严重得多,正常的数学运算至少要从的可表示域中选择最佳值

double
。这意味着当您使用这些方法时,您可以有更多的圆边界穿越事件。



欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5426897.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-11
下一篇 2022-12-11

发表评论

登录后才能评论

评论列表(0条)

保存