【问题标题】:losing precision converting from java BigDecimal to double丢失从 java BigDecimal 转换为 double 的精度
【发布时间】:2011-04-21 20:45:07
【问题描述】:

我正在使用一个完全基于双精度的应用程序,并且在一种将字符串解析为双精度的实用程序方法中遇到了问题。我找到了一个解决方法,使用 BigDecimal 进行转换可以解决问题,但是当我将 BigDecimal 转换回双精度时会引发另一个问题:我失去了几个精度位置。例如:

import java.math.BigDecimal;
import java.text.DecimalFormat;

public class test {
    public static void main(String [] args){
        String num = "299792.457999999984";
        BigDecimal val = new BigDecimal(num);
        System.out.println("big decimal: " + val.toString());
        DecimalFormat nf = new DecimalFormat("#.0000000000");
        System.out.println("double: "+val.doubleValue());
        System.out.println("double formatted: "+nf.format(val.doubleValue()));
    }
}

这会产生以下输出:

$ java test
big decimal: 299792.457999999984
double: 299792.458
double formatted: 299792.4580000000

格式化的双精度表明它在第三位之后失去了精度(应用程序需要那些较低的精度位)。

我怎样才能让 BigDecimal 保留这些额外的精确位置?

谢谢!


赶上这篇文章后更新。有几个人提到这超出了双精度数据类型的精度。除非我读错了这个参考: http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.2.3 那么双原语的最大指数值为 Emax = 2K-1-1,标准实现有 K=11。那么,最大指数应该是511吧?

【问题讨论】:

    标签: java floating-point double precision bigdecimal


    【解决方案1】:

    您已达到具有该数字的double 的最大精度。这是不可能的。在这种情况下,该值被四舍五入。从BigDecimal 的转换是不相关的,精度问题是相同的。例如:

    System.out.println(Double.parseDouble("299792.4579999984"));
    System.out.println(Double.parseDouble("299792.45799999984"));
    System.out.println(Double.parseDouble("299792.457999999984"));
    

    输出是:

    299792.4579999984
    299792.45799999987
    299792.458
    

    对于这些情况,double 的小数点后精度超过 3 位。它们恰好是您的号码的零,这是您可以放入double 的最接近的表示。在这种情况下,它更接近四舍五入,所以你的 9 似乎消失了。如果你试试这个:

    System.out.println(Double.parseDouble("299792.457999999924"));
    

    您会注意到它保留了您的 9,因为它更接近四舍五入:

    299792.4579999999
    

    如果您要求保留号码中的所有位数字,则必须更改在double 上运行的代码。您可以使用BigDecimal 代替它们。如果您需要性能,那么您可能想探索BCD 作为一个选项,尽管我不知道有任何库。


    响应您的更新:双精度浮点数的最大指数实际上是 1023。不过,这不是您的限制因素。您的数字超过了表示有效数字的 52 个小数位的精度,请参阅IEEE 754-1985

    使用this floating-point conversion 以二进制形式查看您的号码。指数为 18,因为 262144 (2^18) 最接近。如果你取小数位并在二进制中加一或减一,你会发现没有足够的精度来表示你的数字:

    299792.457999999900 // 0010010011000100000111010100111111011111001110110101
    299792.457999999984 // here's your number that doesn't fit into a double
    299792.458000000000 // 0010010011000100000111010100111111011111001110110110
    299792.458000000040 // 0010010011000100000111010100111111011111001110110111
    

    【讨论】:

    • 几乎正确;改变舍入模式,你就解决了OP的问题
    • @Anon:你在说什么?使用double 无法解决 OP 的问题。
    • 查看我的答案,或重新阅读 OP 的问题以了解他/她失去 9 的问题
    • @Anon:向上或向下舍入都没有关系,精度仍然会丢失。 OP声明必须保持精度。更多的 9 并不意味着更精确,它只是意味着你正在四舍五入。
    • 也许 OP 错误地认为精度只保留到小数点后 3 位。如果 OP 在这些之后只需要几位数字,那么 0 实际上是最接近的表示。在这种情况下,没有什么可做的。强制取整会失去更多的精度。
    【解决方案2】:

    问题是double 可以容纳15 位数字,而BigDecimal 可以容纳任意数字。当您调用toDouble() 时,它会尝试应用舍入模式来删除多余的数字。但是,由于输出中有很多 9,这意味着它们会一直向上舍入为 0,并带有进位到下一个最高位。

    为了尽可能保持精度,您需要更改 BigDecimal 的舍入模式以使其截断:

    BigDecimal bd1 = new BigDecimal("12345.1234599999998");
    System.out.println(bd1.doubleValue());
    
    BigDecimal bd2 = new BigDecimal("12345.1234599999998", new MathContext(15, RoundingMode.FLOOR));
    System.out.println(bd2.doubleValue());
    

    【讨论】:

      【解决方案3】:

      只打印那么多数字,以便在将字符串解析回 double 时,将得到完全相同的值。

      一些细节可以在Double#toString的javadoc中找到

      m 或 a 的小数部分必须打印多少位?必须至少有一个数字来表示小数部分,除此之外,必须有尽可能多的数字,但仅能将参数值与相邻的 double 类型值区分开来。也就是说,假设 x 是由该方法为有限非零参数 d 生成的十进制表示所表示的精确数学值。那么 d 必须是最接近 x 的 double 值;或者如果两个 double 值同样接近 x,则 d 必须是其中之一,并且 d 的有效位的最低有效位必须为 0。

      【讨论】:

        【解决方案4】:

        如果它完全基于双打......你为什么使用BigDecimalDouble 不是更有意义吗?如果它的价值太大(或精度太高)那么......你不能转换它;这就是首先使用 BigDecimal 的原因。

        至于为什么会丢失精度,来自javadoc

        将此 BigDecimal 转换为双精度数。这种转换类似于 Java 语言规范中定义的从 double 到 float 的窄化原始转换:如果这个 BigDecimal 具有太大的幅度表示为 double,它将酌情转换为 Double.NEGATIVE_INFINITY 或 Double.POSITIVE_INFINITY。请注意,即使返回值是有限的,这种转换也会丢失有关 BigDecimal 值精度的信息。

        【讨论】:

        • 这适用于OP的问题如何?您是否查看过 JLS 以了解缩小原始转换的范围?
        【解决方案5】:

        您已经达到了双精度的最大可能精度。如果您仍想将值存储在原语中...一种可能的方法是将小数点之前的部分存储在 long 中

        long l = 299792;
        double d = 0.457999999984;
        

        由于您没有用完(这是一个不好的词选择)存储小数部分的精度,因此您可以为小数部分保留更多位的精度。这应该很容易通过一些舍入等来完成。

        【讨论】:

        • 但通常更好的解决方案是在整个过程中使用BigDecimal,它旨在用于任意精度的数字。
        • 同意,尽管不知何故我觉得 OP 正在寻找 BigDecimal 的替代品。
        • 这会丢失大数的 double 的指数范围,并且不利于小数。如果您打算沿着这条路线走,那么使用“定点”更有意义(例如,小数部分作为分子超过 2^63 或 2^64)。或者,将数字表示为双精度数的总和(即近似值加上一些误差项);甚至有一些方法可以对此进行准确的求和(例如在 Python 的 [math.fsum()] 中),但我不确定这对乘法/除法的推广有多容易。
        猜你喜欢
        • 2012-09-05
        • 2010-10-29
        • 2013-12-31
        • 2018-10-05
        • 2019-10-23
        • 1970-01-01
        • 2016-06-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多