从Java的BigDecimal转换为double时失去精度

18

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

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

这个格式化后的double值在第三位后就失去了精度(应用程序需要更高的精度位数)。

我该如何让 BigDecimal 保持这些额外的精度位数呢?

谢谢!


更新:在阅读完本帖后,有几个人提到这超出了 double 数据类型的精度。除非我错误地阅读了这份参考文档: http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.2.3 然而,double 原始数据类型具有指数最大值 Emax = 2K-1-1,并且标准实现中 K=11。因此,最大指数应该是511,不是吗?

5个回答

22

使用该数字,您已达到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

如果您需要保留数字中的所有位数,则需要更改操作双精度浮点数的代码。您可以使用BigDecimal替换它们。如果需要性能,则可以探索BCD作为一种选择,尽管我不知道有没有现成的库。
针对您更新的内容:双精度浮点数的最大指数实际上是1023。但这里不是您的限制因素。您的数字超过了表示有效数字的52个小数位的精度,请参阅IEEE 754-1985
使用此浮点数转换工具以二进制形式查看您的数字。指数为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
4
@Anon:你在说什么?使用“double”无法解决OP的问题。 - WhiteFang34
1
@Anon:无论你是四舍五入还是直接舍去,精度都会丢失。OP 表示必须保留精度。更多的 9 并不意味着更高的精度,只是意味着你正在向下取整。 - WhiteFang34
1
也许OP错误地认为精度仅在小数点后保留3位。如果OP只需要在这些数字后再多几个数字,那么0实际上是最接近的表示。在这种情况下,无需进行任何操作。强制向下舍入会损失比必要更多的精度。 - WhiteFang34
现在你指出来后,这很明显。我确实需要包含所有精度位数,但它似乎保留了11个位置——通过四舍五入——这可能足够了。如果不够的话,我将需要重构为BigDecimal。谢谢! - Edward Q. Bridges
显示剩余3条评论

5
问题在于一个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#toString的javadoc

对于m或a的小数部分,必须至少打印一个数字,并且除此以外需要尽可能多的数字,但仅需要尽可能多的数字,可以唯一地区分类型double的相邻值。也就是说,假设x是由此方法生成的十进制表示形式表示的有限非零参数d所代表的精确数学值。然后,d必须是最接近x的double值;或者如果两个double值与x等距,则d必须是它们之一,并且d的有效位数中最不重要的位必须为0。


0

你已经达到了双精度浮点数的最大精度。如果你仍然想将该值存储在原始数据类型中... 一种可能的方法是将小数点前的部分存储在 long 类型中。

long l = 299792;
double d = 0.457999999984;

由于您没有使用精度(这是一个糟糕的选择),来存储小数部分,因此您可以为分数部分保留更多的精度位数。这应该很容易通过一些四舍五入等方式实现。


2
但是一般来说更好的解决方案是始终使用 BigDecimal,它旨在用于任意精度数字。 - Andrzej Doyle
同意,尽管我有点印象是OP在寻找一个替代BigDecimal的选择。 - Java Drinker
这会失去双精度浮点数的指数范围,对于大数没有好处,而对于小数也没有好处。如果你要走这条路,更有意义的做法是使用“定点数”(分数部分作为分子,分母为2^63或2^64)。或者,将数字表示为双精度浮点数的和(即近似值加上一些误差项);甚至有方法可以进行准确的求和(例如在Python的[math.fsum()]中),但我不确定这如何推广到乘除运算。 - tc.

0
如果完全基于双精度数,那么为什么要使用BigDecimal?使用Double更有意义吧?如果值太大(或精度太高),则无法进行转换;这就是首选使用BigDecimal的原因。
至于为什么会失去精度,可以从javadoc中看到:
将此BigDecimal转换为double。该转换类似于Java语言规范中定义的从double到float的缩小原始转换:如果此BigDecimal具有太大的数量级表示为double,则会相应地将其转换为Double.NEGATIVE_INFINITY或Double.POSITIVE_INFINITY。请注意,即使返回值是有限的,此转换也可能会丢失有关BigDecimal值精度的信息。

这与楼主的问题有什么关系?您是否查阅了JLS以了解有关缩小原始转换的知识? - Anon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接