Java中的BigDecimal精度问题

15

我知道下面的问题早已存在,但我仍然不理解。

System.out.println(0.1 + 0.1 + 0.1);    

即便我使用 BigDecimal

System.out.println(new BigDecimal(0.1).doubleValue()
    + new BigDecimal(0.1).doubleValue()
    + new BigDecimal(0.1).doubleValue());

为什么结果是:0.30000000000000004 而不是:0.3

如何解决这个问题?


14
你没有使用 BigDecimal,实际上你在做和添加双精度浮点数一模一样的事情。doubleValue() 返回的是一个双精度浮点数。请参考 BigDecimal 的 Javadoc 了解如何进行加减等操作。 - Brian Roach
关于double计算问题,请查看《计算机科学家应该了解的浮点数算术知识》。Java通过提供输出格式选项来解决这个问题。 - Andrew Thompson
你为什么要使用 doubleValue()???我以为你想要一个十进制类型。 - Kerrek SB
5个回答

32

你实际想要的是

new BigDecimal("0.1")
 .add(new BigDecimal("0.1"))
 .add(new BigDecimal("0.1"));

new BigDecimal(double) 构造函数会获取 double 类型所具有的不精确性,因此当你输入 0.1 时,舍入误差已经被引入。使用 String 构造函数可以避免通过 double 转换时出现的舍入误差。


谢谢你的一切!现在...我能理解了。´ // 对于我的问题有效 Double value= 0.1d; BigDecimal total = new BigDecimal("0.0"); for (int i = 0; i < 9; i++) { total = total.add(new BigDecimal(value.toString())); } System.out.print(total);// 不起作用 System.out.print(0.1d + 0.1d + 0.1d);// 不起作用 System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.1)).add(new BigDecimal(0.1)));// 有效 System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.1")).add(new BigDecimal("0.1")));// 有效,但精度较低 System.out.println(0.1f + 0.1f + 0.1f); ` - Jefferson
1
稍作更正:“double的不精确性”——从技术上讲,这并不是真的。双精度浮点数可以完美地表示0.1——它具有1的尾数和-1的指数。将double转换为BigDecimal时,会在某个地方将浮点表示转换为普通二进制表示。这就是为什么double构造函数存在缺陷的原因。 - theeggman85
4
什么?不,它不能。在二进制中,0.1无法表示为一个分数,这就是double的表示方式。一个尾数为1和指数为-1可以得到0.5。 - Louis Wasserman
3
没错,我的错误。我错误地阅读了一个双精度数的二进制表示。 - theeggman85

11

首先,绝对不要使用BigDecimal的双精度构造函数。在少数情况下可能是正确的选择,但大多数情况下都不是。

如果您可以控制输入,请使用BigDecimal字符串构造函数,正如已经提出的那样。这样,您就可以得到确切想要的结果。如果您已经有一个双精度数(毕竟这种情况很常见),请勿使用双精度构造函数,而是使用静态的valueOf方法。这样做的好处是,我们得到了双精度数的规范表示形式,这至少可以缓解问题...而且结果通常更加直观。


5
这不是Java的问题,而是计算机普遍存在的问题。核心问题在于从十进制格式(人类格式)转换为二进制格式(计算机格式)。一些十进制数字在没有无限循环小数的情况下是不能用二进制格式表示的。
例如,0.3的十进制是0.01001100...的二进制。但是计算机有限的“槽位”(位数)无法保存所有完整的无限表示。它只保存0.01001100110011001100(例如)。但是该数字在十进制中不再是0.3,而是0.30000000000000004。

3
你面临的问题是0.1被表示为稍微更高的数字,例如:
System.out.println(new BigDecimal(0.1));

打印

0.1000000000000000055511151231257827021181583404541015625

Double.toString() 方法会考虑到此表示误差,因此您看不到它。
同样地,0.3被表示为比其实际值稍低的值。
0.299999999999999988897769753748434595763683319091796875

如果你将0.1的表示值乘以3,则无法得到0.3的表示值,而是会得到稍微高一点的值。

0.3000000000000000166533453693773481063544750213623046875

这不仅是表示错误,还包括操作引起的舍入误差。这种误差超过了Double.toString()可以纠正的范围,因此您会看到舍入误差。

故事的寓意是,如果您使用float或double也请适当四舍五入解决问题。

double d = 0.1 + 0.1 + 0.1;
System.out.println(d);
double d2 = (long)(d * 1e6 + 0.5) / 1e6; // round to 6 decimal places.
System.out.println(d2);

打印

0.30000000000000004
0.3

3

试试这个:

BigDecimal sum = new BigDecimal(0.1).add(new BigDecimal(0.1)).add(new BigDecimal(0.1));

编辑:实际上,查看Javadoc后,这将与原始问题相同。构造函数BigDecimal(double)将创建与0.1的精确浮点表示对应的BigDecimal,该值并不完全等于0.1。

然而,由于整数可以在浮点表示中始终被精确表示,因此这将给出精确结果:

BigDecimal one = new BigDecimal(1);
BigDecimal oneTenth = one.divide(new BigDecimal(10));

BigDecimal sum = oneTenth.add(oneTenth).add(oneTenth);

7
不要在大数字计算中使用 double 构造函数(除非某些罕见情况,但基本上这是一个糟糕的主意)。如果可以,请使用字符串构造函数(这将是准确的),如果你已经有一个双精度浮点数,请使用 valueOf 方法,以避免出现附加虚假精度。 - Voo

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