使用BigDecimal作为货币的一个现实例子明显优于使用double

maa*_*nus 44 java floating-point currency biginteger

我们知道使用double货币是容易出错的,不推荐使用.但是,我还没有看到一个现实的例子,BigDecimal虽然double失败了,但不能通过一些舍入来简单地修复.


注意琐碎的问题

double total = 0.0;
for (int i = 0; i < 10; i++) total += 0.1;
for (int i = 0; i < 10; i++) total -= 0.1;
assertTrue(total == 0.0);
Run Code Online (Sandbox Code Playgroud)

不计算,因为它们通过舍入(在这个例子中从0到16的小数位的任何东西)都可以解决.


涉及总结大值计算可能需要一些中间圆棒,但由于流通总货币之中USD 1e12,爪哇double(即标准IEEE双精度其15个十进制数字)仍然是足够美分事件.


涉及分工的计算通常是不精确的,即使是BigDecimal.我可以构造一个不能用doubles 执行的计算,但可以BigDecimal使用100的标度执行,但它不是你在现实中可以遇到的东西.


我没有声称这样一个现实的例子不存在,只是我还没有看到它.

我也肯定同意,使用double更容易出错.

我正在寻找的是如下方法(根据Roland Illig的回答)

/** 
  * Given an input which has three decimal places,
  * round it to two decimal places using HALF_EVEN.
*/
BigDecimal roundToTwoPlaces(BigDecimal n) {
    // To make sure, that the input has three decimal places.
    checkArgument(n.scale() == 3);
    return n.round(new MathContext(2, RoundingMode.HALF_EVEN));
}
Run Code Online (Sandbox Code Playgroud)

和一个像测试一样

public void testRoundToTwoPlaces() {
    final BigDecimal n = new BigDecimal("0.615");
    final BigDecimal expected = new BigDecimal("0.62");
    final BigDecimal actual = roundToTwoPlaces(n);
    Assert.assertEquals(expected, actual);
}
Run Code Online (Sandbox Code Playgroud)

当使用时被天真地重写时double,测试可能会失败(它不是针对给定的输入,而是针对其他输入).但是,它可以正确完成:

static double roundToTwoPlaces(double n) {
    final long m = Math.round(1000.0 * n);
    final double x = 0.1 * m;
    final long r = (long) Math.rint(x);
    return r / 100.0;
}
Run Code Online (Sandbox Code Playgroud)

它很丑陋且容易出错(并且可能会简化),但它可以很容易地封装在某个地方.这就是为什么我在寻找更多的答案.

Bee*_*ope 27

double在处理货币计算时,我可以看到四种可能会让你感到困惑的基本方法.

螳螂太小了

尾数中精度约为15位十进制数,只要你处理的金额大于此值,你就会得到错误的结果.如果你跟踪美分,问题将在10 13(10万亿美元)之前开始出现.

虽然这是一个很大的数字,但并不是那么大.美国的国内生产总值约为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)
Run Code Online (Sandbox Code Playgroud)

三角洲(160年之间的差异doubleBigDecimal首次命中率相差> 1美分,大约2万亿(从现在开始160年可能不会那么多),当然只会变得更糟.

当然,53位的Mantissa意味着这种计算的相对误差可能非常小(希望你的工作不会超过2万亿美元).实际上,在大多数示例中,相对误差基本上保持稳定.你当然可以组织它,以便你(例如)在尾数中减去两个不同的精度,导致一个任意大的错误(练习到读者).

改变语义

所以你认为你非常聪明,并设法提出一个舍入方案,让你可以double在本地JVM上使用并详尽测试你的方法.继续部署它.明天或下周,或者每当你最糟糕的时候,结果都会改变,你的技巧也会破裂.

与几乎所有其他基本语言表达式不同,当然与整数或BigDecimal算术不同,默认情况下,由于strictfp功能,许多浮点表达式的结果没有单个标准定义值.平台可以自由选择使用更高精度的中间体,这可能会导致不同硬件,JVM版本等产生不同的结果.对于相同的输入,当方法从解释转换为JIT时,结果甚至可能会在运行时发生变化 -compiled!

如果您在Java之前的1.2天编写了代码,那么当Java 1.2突然引入now-default变量FP行为时,您会非常生气.你可能只想strictfp在任何地方使用它,并希望你不要遇到任何相关的错误 - 但在某些平台上,你会丢掉大部分双重购买你的性能.

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

不精确的表示

正如Roland在他的回答中所指出的,一个关键问题double是它没有对某些大多数非整数值的精确表示.虽然0.1在某些情况下(例如Double.toString(0.1).equals("0.1")),单个非精确值通常会"往返" ,但只要对这些不精确的值进行数学计算,错误就会复合,这可能是不可恢复的.

特别是,如果您"接近"舍入点,例如~1.005,则可能得到值1.00499999 ...当真值为1.0050000001时,反之亦然.因为错误是双向的,所以没有可以解决这个问题的舍入魔法.没有办法判断是否应该增加1.004999999 ......的值.你的roundToTwoPlaces()方法(一种双舍入)只能起作用,因为它处理的情况是1.0049999应该被提升,但它永远不能越过边界,例如,如果累积误差导致1.0050000000001变成1.00499999999999它不能修理它.

你不需要大或小的数字来达到这个目的.你只需要一些数学,结果就会接近边界.你做的数学越多,与真实结果的可能偏差越大,跨越边界的可能性就越大.

按此处要求进行简单计算的搜索测试:amount * tax并将其舍入为2位小数(即美元和美分).有一些舍入方法,目前使用的roundToTwoPlacesB是你的1的加强版本(通过增加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
Run Code Online (Sandbox Code Playgroud)

请注意,"原始结果"(即精确的未结果结果)始终接近x.xx5000边界.你的舍入方法在高低两侧都会出错.你无法解决这个问题.

不精确的计算

有些java.lang.Math方法不需要正确的舍入结果,而是允许高达2.5 ulp的错误.当然,你可能都不会被使用双曲线函数与多币种,但功能,如exp()pow()经常发现他们的方式进入货币计算,这些只有1个ULP的精确度.因此,返回时该数字已经"错误".

这与"不精确表示"问题相互作用,因为这种类型的错误比正常数学运算严重得多,正常数学运算至少从可表示域中选择最佳可能值double.这意味着当您使用这些方法时,您可以拥有更多的圆边界交叉事件.


Rol*_*lig 20

当你四舍五入double price = 0.615到两位小数时,得到0.61(向下舍入),但可能预期为0.62(因为5而被四舍五入).

这是因为双0.615实际上是0.6149999999999999911182158029987476766109466552734375.


Hen*_*nry 11

您在实践中遇到的主要问题与round(a) + round(b)不一定相等的事实有关round(a+b).通过使用BigDecimal您可以很好地控制舍入过程,因此可以使您的总和正确.

当您计算税收时,例如18%的增值税,当准确表示时,很容易获得具有两个以上小数位的值.四舍五入成为一个问题.

让我们假设你买了2篇文章,每篇1.3美元

Article  Price  Price+VAT (exact)  Price+VAT (rounded)
A        1.3    1.534              1.53
B        1.3    1.534              1.53
sum      2.6    3.068              3.06
exact rounded   3.07
Run Code Online (Sandbox Code Playgroud)

因此,如果您使用double进行计算并且仅使用round来打印结果,那么您将获得总计3.07,而账单上的金额实际上应该是3.06.


Gho*_*ica 9

让我们在这里给出一个"技术性较低,更具哲学性"的答案:为什么你认为"Cobol"在处理货币时不使用浮点运算?!

(引用中的"Cobol",如:解决现实世界业务问题的现有遗留方法).

含义:差不多50年前,当人们开始使用计算机进行业务而非金融工作时,他们很快意识到"浮点"表示对金融行业不起作用(可能期望在问题中指出一些罕见的利基角落) ).

请记住:那时候,抽象真的很贵!它的价格足够昂贵,并且在那里有一个寄存器; 我们站在他们的肩膀上的巨人们很快就会明白......使用"浮点"并不能解决他们的问题; 并且他们不得不依赖别的东西; 更抽象 - 更贵!

我们的行业有50多年的时间来提出"适用于货币的浮动点" - 而且常见的答案仍然是:不要这样做.相反,你转向BigDecimal等解决方案.


use*_*421 6

你不需要一个例子.你只需要第四种形式的数学.浮点中的分数以二进制基数表示,二进制基数与十进制基数不可比较.十年级的东西.

因此,总会有舍入和近似,并且在任何方式,形状或形式的会计中都不可接受.这些书必须平衡到最后一分钱,因此,FYI在每天结束时和整个银行定期进行银行分行.

遭受四舍五入错误的表达不计算在内

荒谬.这问题所在.排除舍入误差排除了整个问题.