【问题标题】:Is it sufficient to convert a double to a BigDecimal just before addition to retain original precision?在加法之前将 double 转换为 BigDecimal 是否足以保持原始精度?
【发布时间】:2019-01-02 18:11:13
【问题描述】:

我们正在解决与数值精度相关的错误。我们的系统收集一些数字并吐出它们的总和。 问题是 系统不保留数字精度, 例如300.7 + 400.9 = 701.599...,而预期结果为 701.6。精度应该适应输入值,所以我们不能只将结果四舍五入到固定精度。

问题很明显,我们使用双精度值,加法累加了十进制值的二进制表示的误差。

数据的路径如下:

  1. XML 文件,类型 xsd:decimal
  2. 解析成 java 原始双精度。它的 15 位小数应该足够了,我们希望值总计不超过 10 位,5 个小数位。
  3. 存入DB MySql 5.5, type double
  4. 通过 Hibernate 加载到 JPA 实体中,即仍然是原始的 double
  5. 这些值的总和
  6. 将总和打印到另一个 XML 文件中

现在,我假设最佳解决方案是将所有内容都转换为十进制格式。毫不奇怪,采用最便宜的解决方案是有压力的。事实证明,在以下示例中的情况 B 中,在添加几个数字之前将双精度数转换为 BigDecimal 有效:

import java.math.BigDecimal;

public class Arithmetic {
    public static void main(String[] args) {
        double a = 0.3;
        double b = -0.2;
        // A
        System.out.println(a + b);//0.09999999999999998
        // B
        System.out.println(BigDecimal.valueOf(a).add(BigDecimal.valueOf(b)));//0.1
        // C
        System.out.println(new BigDecimal(a).add(new BigDecimal(b)));//0.099999999999999977795539507496869191527366638183593750
    }
}

更多信息: Why do we need to convert the double into a string, before we can convert it into a BigDecimal? Unpredictability of the BigDecimal(double) constructor

我担心这样的解决方法会成为一颗定时炸弹。 首先,我不太确定这种算法是否适用于所有情况。 其次,仍然存在一些风险,将来有人可能会实施一些更改并将 B 更改为 C,因为这个陷阱远非显而易见,甚至单元测试也可能无法揭示错误。

我愿意接受第二点,但问题是:这种解决方法会提供正确的结果吗?会不会有某种情况以某种方式

Double.valueOf("12345.12345").toString().equals("12345.12345")

是假的吗?鉴于根据 javadoc,Double.toString 只打印唯一表示底层 double 值所需的数字,所以当再次解析时,它会给出相同的 double 值?对于我只需要添加数字并使用这种神奇的Double.toString(Double d) 方法打印总和的用例来说,这还不够吗?需要明确的是,我确实更喜欢我认为的干净的解决方案,在任何地方都使用 BigDecimal,但我有点缺乏出售它的论据,我的意思是理想情况下在添加之前转换为 BigDecimal 的示例未能完成上述工作。

【问题讨论】:

  • "2. 解析成 java 原语双精度" 当你这样做的时候,你已经失去了精度。立即将其解析为BigDecimal
  • 如果您需要保证精度,double 不应用于处理的任何部分。
  • 在将数据解析为 double 的那一刻,您就会失去精确度。 double 无法精确地包含值 0.2。它根本无法以二进制格式表示。但它是“正确”打印的,因为字符串转换考虑到了这一点并对其进行了四舍五入。这就是为什么double 的直接转换是不可预测的。
  • 他们说的:一碰double,它就坏了。仅限 BigDecimals,而且只有当您直接从字符串转换时才会这样做:BigDecimal.valueOf("0.3"),而不是 BigDecimal.valueOf(0.3)
  • 是的,浮点格式在内部不能精确地表示十进制值。无论如何,双精度足以容纳一个 15 位数字,这样当例如从 String 解析为 double 的 10 位值使用 Double.toString 中的特殊舍入算法再次打印到 String,我应该产生与我首先解析 double 相同的值,对吗?然后这个 String 用于构造 BigDecimal,BigDecimal 又用于数学运算。我错过了其他一些陷阱?

标签: java double precision bigdecimal


【解决方案1】:

如果无法避免解析为原始double 或存储为double,则应尽早转换为BigDecimal

double 不能准确地表示小数。 double x = 7.3; 中的值永远不会是 7.3,而是非常接近它的值,从第 16 位到右边(给出 50 个小数位左右)可见差异。不要被打印可能正好给出“7.3”这一事实误导,因为打印已经进行了某种四舍五入并且没有准确显示数字。

如果您对 double 数字进行大量计算,那么微小的差异最终会相加,直到超出您的容忍度。因此,在需要小数的计算中使用双精度确实是一颗定时炸弹。

[...] 我们期望的值总计不超过 10 位,5 个小数位。

我读到这个断言意味着你处理的所有数字都是0.00001 的精确倍数,没有任何其他数字。您可以使用

将双精度数转换为此类 BigDecimals
new BigDecimal.valueOf(Math.round(doubleVal * 100000), 5)

这将为您提供具有 5 个小数位的数字的精确表示,即最接近输入 doubleVal 的 5 个小数位。通过这种方式,您可以纠正 doubleVal 与您最初的意思的十进制数之间的微小差异。

如果您只是简单地使用BigDecimal.valueOf(double val),您将通过您正在使用的双精度的字符串表示,这不能保证它是您想要的。它取决于 Double 类内部的舍入过程,该过程试图用最合理的小数位数表示 7.3 的双重近似值(可能是 7.30000000000000123456789123456789125)。它恰好导致“7.3”(并且,对开发人员的赞誉,经常匹配“预期”字符串)而不是“7.300000000000001”或“7.3000000000000012”,这对我来说似乎同样合理。

这就是为什么我建议不要依赖这种四舍五入,而是自己进行四舍五入,方法是小数移动 5 位,然后四舍五入到最接近的长整数,并构建一个 BigDecimal 缩减 5 个小数位。这可以保证您获得(最多)5 个小数位的精确值。

然后使用 BigDecimals 进行计算(如有必要,使用适当的 MathContext 进行舍入)。

当您最终必须将数字存储为双精度时,请使用BigDecimal.doubleValue()。得到的双精度数将足够接近小数点,上述转换肯定会给您提供与以前相同的 BigDecimal (除非您有非常大的数字,例如小数点前 10 位数字 - 您迷失了 @987654333 @反正)。

附:请务必仅在 十进制 分数与您相关时使用 BigDecimal - 有时英国先令货币由 12 便士组成。将分数磅表示为 BigDecimal 会比使用双精度值更糟糕。

【讨论】:

  • 解释得很好。如果可以的话,我会 +2。
  • 谢谢,你真好 :-)
  • 感谢您的详细说明!为什么转换前需要移动小数点?根据 javadoc 和一个带有几个任意数字的测试,BigDecimal.valueOf,应该提供正确的值,虽然我不能真正测试所有的数字 :-) 顺便说一句,我假设你建议使用工厂方法和 new 关键字是那里是一个错误。
【解决方案2】:

这取决于您使用的数据库。如果您使用的是 SQL Server,则可以将数据类型用作 numeric(12, 8),其中 12 表示数值,8 表示精度。同样,对于我的 SQL DECIMAL(5,2),您可以使用。

如果您使用上述数据类型,您不会丢失任何精度值。

Java Hibernate 类:

你可以定义 私人双纬;

数据库:

【讨论】:

  • 您忽略了这个数字首先被解析为double 的事实,因此已经失去了精度。在数据库中使用的类型不太重要 - 所有数据库都有一种表示精确十进制数的方法。
  • @RealSkeptic 转换为字符串后,您可以可靠地获得开始的小数点,具体取决于您需要多少位数。
猜你喜欢
  • 2012-09-05
  • 1970-01-01
  • 2019-02-24
  • 1970-01-01
  • 2016-07-23
  • 2014-02-02
  • 2011-03-30
  • 1970-01-01
  • 2015-03-31
相关资源
最近更新 更多