如何解决Java中的Double舍入问题

88

似乎减法触发了某种问题,导致结果值不正确。

double tempCommission = targetPremium.doubleValue()*rate.doubleValue()/100d;

78.75 = 787.5 * 10.0/100d

double netToCompany = targetPremium.doubleValue() - tempCommission;

708.75 = 787.5 - 78.75

double dCommission = request.getPremium().doubleValue() - netToCompany;

877.8499999999999 = 1586.6 - 708.75

得出的期望值为877.85。

为确保正确计算,应该采取什么措施?

13个回答

106

要控制浮点数的精度,应该使用java.math.BigDecimal。阅读John Zukowski的The need for BigDecimal以获得更多信息。

根据您的示例,使用BigDecimal的最后一行如下所示。

import java.math.BigDecimal;

BigDecimal premium = BigDecimal.valueOf("1586.6");
BigDecimal netToCompany = BigDecimal.valueOf("708.75");
BigDecimal commission = premium.subtract(netToCompany);
System.out.println(commission + " = " + premium + " - " + netToCompany);

这将导致以下输出。

877.85 = 1586.6 - 708.75

11
无法完全“避免”浮点数算术错误。表示数字所使用的位数始终是有限的。你能做的只是使用更高精度(位数)的数据类型。 - Ates Goral
1
没错。我会编辑我的答案,以更准确地反映BigDecimal的使用。 - Eric Weilnau
6
我会添加一条注释,说明BigDecimal division 需要与+、-、* 处理方式略有不同,因为默认情况下,如果无法返回精确值(例如1/3),它将抛出异常。在类似的情况下,我使用了以下方法:BigDecimal.valueOf(a).divide(BigDecimal.valueOf(b), 25, RoundingMode.HALF_UP).doubleValue()。其中25是最大精度位数(超过double结果所需的精度)。 - Joshua Goldberg

77

如前面的答案所述,这是使用浮点数算术运算的后果。

正如前面的帖子建议的那样,在进行数字计算时,请使用 java.math.BigDecimal

然而,使用 BigDecimal 也有一个陷阱。当你从 double 值转换为 BigDecimal 时,你可以选择使用新的 BigDecimal(double) 构造函数或 BigDecimal.valueOf(double) 静态工厂方法。请使用静态工厂方法。

该 double 构造函数将整个 double 的精度转换为 BigDecimal,而静态工厂方法有效地将其转换为字符串,然后将其转换为 BigDecimal

当你遇到那些微妙的舍入误差时,这就变得相关了。一个数可能显示为 .585,但内部的值是“0.58499999999999996447286321199499070644378662109375”。如果你使用 BigDecimal 构造函数,则会得到不等于 0.585 的数字,而静态方法会给你一个等于 0.585 的值。

double value = 0.585;
System.out.println(new BigDecimal(value));
System.out.println(BigDecimal.valueOf(value));

在我的系统上输出:

0.58499999999999996447286321199499070644378662109375
0.585

6
我遇到过这个问题很多次,真的很烦人! - Richard
4
请注意,原因在于静态方法使用了一个无限制的MathContext,这实际上意味着:new BigDecimal(value, new MathContext(0, RoundingMode.HALF_UP)) ;) - radekEm

10

另一个例子:

double d = 0;
for (int i = 1; i <= 10; i++) {
    d += 0.1;
}
System.out.println(d);    // prints 0.9999999999999999 not 1.0

使用BigDecimal替代。

编辑:

此外,要指出的是,这不是一个“Java”舍入问题。其他语言也会表现出类似的(虽然不一定始终如一)行为。至少在这方面,Java保证了一致的行为。


8
我会将上面的例子修改如下:

我会将上述示例修改如下:

import java.math.BigDecimal;

BigDecimal premium = new BigDecimal("1586.6");
BigDecimal netToCompany = new BigDecimal("708.75");
BigDecimal commission = premium.subtract(netToCompany);
System.out.println(commission + " = " + premium + " - " + netToCompany);

这样做可以避免一开始使用字符串时的陷阱。 另一个选择:
import java.math.BigDecimal;

BigDecimal premium = BigDecimal.valueOf(158660, 2);
BigDecimal netToCompany = BigDecimal.valueOf(70875, 2);
BigDecimal commission = premium.subtract(netToCompany);
System.out.println(commission + " = " + premium + " - " + netToCompany);

我认为这些选项比使用双精度浮点数更好。在 Web 应用程序中,数字最初就是字符串。


7

任何时候你使用双精度浮点数进行计算,这种情况都可能发生。以下代码将给出877.85的结果:

double answer = Math.round(dCommission * 100000) / 100000.0;


2
最好将其除以100000.0而不是仅仅除以100000;Math.round返回一个长整型,否则你将使用整数除法。 - Michael Myers

4

这是一个有趣的问题。

Timons 的回答的想法是你指定一个表示合法 double 可以达到的最小精度的 epsilon。如果您在应用程序中知道您永远不需要低于 0.00000001 的精度,那么他建议的方法足以获得非常接近真实值的更精确结果。对于预先知道他们的最大精度的应用程序很有用(例如金融货币精度等)。

然而,试图四舍五入的根本问题在于,当您除以一个因子来重新调整比例时,您实际上引入了另一个可能的精度问题。任何 double 的操作都可能引入不同频率的不精确性问题。特别是如果您尝试在非常重要的数字上进行四舍五入(因此您的操作数为 <0),例如,如果您使用 Timons 代码运行以下命令:

System.out.println(round((1515476.0) * 0.00001) / 0.00001);

将导致结果为1499999.9999999998,目标是以500000的单位舍入(即我们想要1500000)

实际上,确保消除不精确性的唯一方法是通过 BigDecimal 进行缩放处理。例如:

System.out.println(BigDecimal.valueOf(1515476.0).setScale(-5, RoundingMode.HALF_UP).doubleValue());

使用epsilon策略和BigDecimal策略的混合将使您对精度有很好的控制。其思想是epsilon让您非常接近,然后BigDecimal将消除由重新缩放引起的任何不准确性。但使用BigDecimal会降低应用程序的预期性能。
有人指出,对于某些用例,使用BigDecimal进行重新缩放的最后一步并不总是必要的,因为您可以确定没有输入值,最终的除法会重新引入误差。目前我不知道如何正确确定这一点,如果有人知道,请告诉我,我将不胜感激。

4

在输出时,保存以分为单位的数字而不是以美元为单位,并进行美元格式化。这样,您可以使用整数,避免精度问题。


这里唯一需要注意的是,在过程的早期可能会丢失一小部分美分,这对于金融应用程序可能不太好。如果这是一个问题,您可以保存1/10美分或任何您需要的精度。 - James Schek
如果我能回到过去,给我的过去自己一个“窍门”,那就是这个。价格以便士计算!(或者1e-3或1e-6——作为整数仍然可以获得200万美元以上的好处) - Trenton

3

请参考这个问题的回答。实际上,你看到的是使用浮点数算术的自然结果。

如果你感觉舒适的话,可以选择一些任意精度(输入的有效数字位数?)并将结果四舍五入。


2

到目前为止,Java中最优雅和高效的方法是:

double newNum = Math.floor(num * 100 + 0.5) / 100;

2
更好的办法是使用JScience,因为BigDecimal的功能相当有限(例如,没有sqrt函数)。
double dCommission = 1586.6 - 708.75;
System.out.println(dCommission);
> 877.8499999999999

Real dCommissionR = Real.valueOf(1586.6 - 708.75);
System.out.println(dCommissionR);
> 877.850000000000

谢谢提供这个库!但对于简单的事情来说,它不是有点过载了吗? - Benj
@Benj 你可以用同样的话说Guava ;) - Tomasz

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