2011-04-21 156 views
11

我正在使用完全基于双精度的应用程序,并且在将字符串解析为双精度的一种实用程序方法中遇到了问题。我发现修复了使用BigDecimal进行转换的问题,但在我将BigDecimal转换回双精度时引发了另一个问题:我失去了几个精度。例如:从java 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 那么双基元的最大指数值为E max = 2 K-1 -1,并且标准实现具有K = 11。所以,最大指数应该是511,不是?

回答

19

您已达到具有该数字的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 
+0

差不多吧;改变舍入模式,你已经解决了OP的问题 – Anon 2011-04-21 21:12:26

+4

@Anon:你在说什么?没有使用'double'来解决OP的问题。 – WhiteFang34 2011-04-21 21:17:35

+0

查看我的答案,或重读OP的问题,以了解他/她失去9的 – Anon 2011-04-21 21:28:36

0

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

至于为什么它失去精度,从javadoc

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

+0

这适用于OP的问题如何?你有没有看过JLS来了解缩小原始转换的内容? – Anon 2011-04-21 21:10:47

0

您已达到双倍的最大可能精度。如果你仍想保存在原语的价值......一种可能的方式是因为您没有使用到的部分小数点前存放在长

long l = 299792; 
double d = 0.457999999984; 

(这词的一个不错的选择)用于存储小数部分的精度,您可以为小数部分保存更多的精度数字。这应该很容易做一些舍入等。

+2

但是一般情况下更好的解决方案是在整个过程中使用'BigDecimal',它旨在用于任意精度数字。 – 2011-05-12 14:02:51

+0

表示同意,尽管不知何故,我得到了OP正在寻找BigDecimal的替代方案的印象。 – 2011-05-13 13:26:25

+0

这会损失大数字的双数的指数范围,并且不会使小数字受益。如果你打算沿着这条路线走下去,那么使用“固定点”(例如,小数部分作为分子超过2^63或2^64)就更有意义了。或者,将该数字表示为双数的总和(即近似值加上一些误差项);有甚至有方法可以用这个方法来做精确的总和(例如在Python的[math.fsum()]中),但我不确定这是多么容易实现乘法/除法。 – 2013-06-10 16:17:14

2

只有那么多的数字被打印,所以,当解析字符串回到双,它会导致完全相同的值。

某些细节可以在Javadoc中找到Double#toString

多少位必须被打印的米或一个小数部分?必须至少有一位数字来表示小数部分,并且除此之外还需要多少位数,但只需要多少位数来唯一地区分参数值和类型为double的相邻值。也就是说,假设x是由该方法对有限非零参数d产生的十进制表示所表示的精确数学值。那么d必须是最接近x的double值;或者如果两个双值同样接近x,那么d必须它们中的一个和d的有效数字的最低位显著必须为0。

3

的问题是,一个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());