当前位置: 首页 > 面试题库 >

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

秦联
2023-03-14
问题内容

我们知道,使用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);

不要算作四舍五入而无法解决的问题(在本例中,从零到小数点后十六位都可以)。

涉及对大值求和的计算可能需要一些中间步骤,但考虑到流通的总货币为USD 1e12Java double(即标准IEEE double
precision
)及其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));
}

加上类似的测试

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);
}

如果使用幼稚地对此进行了重写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;
}

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


问题答案:

我可以看到四种基本方法,这些方法double可以在处理货币计算时给您带来麻烦。

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

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

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

最后,让我们不要以美国为中心。那其他货币呢?一美元的价值约合
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年doubleBigDecimal首次点击之间的差额> 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.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边界。您的舍入方法在高端和低端都有错误。您无法以一般方式修复它。

不精确的计算

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

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



 类似资料:
  • 问题内容: 人们总是被告知永远不要用或类型代表金钱,这一次我向您提出一个问题:为什么? 我敢肯定有一个很好的理由,我根本不知道这是什么。 问题答案: 因为浮点数和双精度数不能准确表示我们用于赚钱的基数10的倍数。这个问题不仅仅针对Java,而且还针对任何使用base 2浮点类型的编程语言。 在基数10中,您可以将10.25编写为1025 * 10 -2(整数乘以10的幂)。IEEE-754浮点数是

  • 我正在尝试使用函数。 而它应该打印$符号或美元? 我使用linux主机。 谢谢

  • 问题内容: 一直有人告诉我,永远不要用或类型来代表金钱,这一次我向您提出一个问题:为什么? 我敢肯定有一个很好的理由,我根本不知道这是什么。 问题答案: 因为浮点数和双精度数不能准确代表我们用于货币的基数10的倍数。这个问题不仅仅针对Java,而且还针对任何使用base 2浮点类型的编程语言。 在基数10中,您可以将10.25编写为1025 * 10 -2(整数乘以10的幂)。IEEE-754浮点

  • 本文向大家介绍java 使用BigDecimal进行货币金额计算的操作,包括了java 使用BigDecimal进行货币金额计算的操作的使用技巧和注意事项,需要的朋友参考一下 float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用 java.math.BigDecimal。 而且使用BigDecimal类也可以进行大数的操作。 方法 类型 描述 public BigDeci

  • 问题内容: 我有一个查询,它从表中返回金额: 这将返回如下金额: 我在碧玉报告中使用了上述金额。 但是,我希望报告中的输出显示为: JRXML代码段是: 我知道我必须使用模式。但是,我无法使其工作。 使用 不管用。 我想念什么?? 谢谢阅读! 问题答案: 你很亲密 应该管用。 您需要使用“¤”字符将其设置为“货币”格式。 如果您下载的iReports版本号与您的服务器相匹配,则会发现您的选择。

  • 在本例中,我们根据美国区域设置和丹麦语区域设置格式化货币。 文件:IOTester.java - 执行上面示例代码,得到以下结果 -