【问题标题】:Performing arithmetic involving non-terminating BigDecimals执行涉及非终止 BigDecimals 的算术
【发布时间】:2018-07-19 07:03:15
【问题描述】:

除以 BigDecimal 时,结果可能是非终止的。因此,您必须提供 MathContext 或 RoundingMode/scale 作为除法运算的一部分。但是,根据算术运算的顺序,精度损失可能会导致差异。

在使用 BigDecimals 时,如何避免由于精度损失(如下面所示的示例)导致的差异?很想知道其他人是如何处理此类问题的。

示例:

BigDecimal v1 = new BigDecimal("29.14");
BigDecimal v2 = new BigDecimal("12");
BigDecimal v3 = new BigDecimal("75");
System.out.println(v1.divide(v2, MathContext.DECIMAL64).multiply(v3).setScale(2, RoundingMode.HALF_UP));
// Prints: 182.12
System.out.println(v1.multiply(v3).divide(v2, MathContext.DECIMAL64).setScale(2, RoundingMode.HALF_UP));
// Prints: 182.13

在上面的例子中,根据操作的顺序,结果会发生变化。

在测试时遇到此示例之前,我从未考虑过 BigDecimal 操作的顺序是一个问题。现在,我陷入了两难境地:)

【问题讨论】:

  • BigDecimal 没有精度损失,不确定的数字是这样的除法不能精确的数字,并以接近结果的周期结束,例如 1 / 3 导致 0.333333...
  • 我认为一个好的方法不是四舍五入,而是简单地削减一些
  • @MarcosVasconcelos 问题是你不能做BigInteger.divide 来获得例如的确切值。 1/3。您必须指定在哪里截断,导致不可避免的精度损失。
  • 我认为我们没有解决此类问题的工具,但发现乘法而不是除法不会产生此类错误: BigDecimal o = new BigDecimal("2"); BigDecimal t = new BigDecimal("0.3333"); BigDecimal t2 = new BigDecimal("3"); /* System.out.println(o.divide(t2)); */ //RuntimeException System.out.println(o.multiply(t)); // 有效
  • 将您的除法精度设置为比您最终想要的高约 5-6 位。我认为需要更多的“额外数字”,但事实证明,5-6 位数字通常就足够了。最后,缩小到所需的精度。请注意,只有除法才是问题。

标签: java precision bigdecimal


【解决方案1】:

这是浮点算术的一个常见问题,无论是二进制还是十进制:除法可能不准确。所以当你必须做一个乘法和除法时,如果你先做除法,结果不准确,当你再做乘法时,你乘以第一个结果的误差。

在您的示例中,确切的结果是 182.125 ,它正好在要四舍五入的边界中 182.13 。当你先进行除法时,29.14/12 的确切结果是 2.428333...

DECIMAL64 的 16 位,四舍五入为 2.428333333333333,乘以 75,得到 182.12499999999997,四舍五入为 182.12

那么这里可以做什么:

  • 如果没有溢出风险,首先进行乘法运算,最后进行除法运算。例如对于a/b*c/d,您应该计算(a*c)/(b*d)
  • 使用小数时尝试添加一位精度数字。在这里,您应该使用 3 个十进制数字的精度,因为初始数字是用 2 个 1 给出的。在这两种情况下,结果都是 182.125

但无论如何,一旦你没有准确的结果,就有可能出现舍入误差,这是浮点计算所固有的。

【讨论】:

  • 或者简单地使用更高的精度并在最后四舍五入到 DECIMAL64。当然,如果可能的话,在除法之前总是做乘法/加法/减法,正如你所说,但更高的精度有很大帮助。能够用简单的泰勒级数计算正弦或余弦就足够了(是的,我知道有更好的方法)。
  • 谢谢!你能解释一下你的部分方法吗?如果我对所有数字使用 3 的精度,我最终会得到 182.100 作为第一个操作顺序的结果。
  • “或者简单地使用更高的精度并舍入到 DECIMAL64”——这是一种有趣的方法。可以在除法中使用 DECIMAL128,然后舍入到 DECIMAL64,然后舍入到 2 位小数:v1.divide(v2, MathContext.DECIMAL128).multiply(v3,MathContext.DECIMAL64).setScale(2, RoundingMode.HALF_UP)。这个问题仍然存在,但它大大降低了它发生的几率。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多