Java中使用双精度数进行取模运算

15

当使用双精度浮点数时,如何解决Java在使用模运算符时出现的奇怪行为?

例如,您期望3.9 - (3.9 % 0.1)的结果应该是3.9(事实上,谷歌也证明了这一点),但是当我在Java中运行它时,我得到的结果是3.8000000000000003

我知道这是由于Java存储和处理双精度浮点数的方式导致的,但是是否有一种方法可以解决这个问题?


1
你想做什么?我有一种感觉,你根本不需要使用“%”。 - polygenelubricants
4个回答

20

如果您需要精确的结果,请使用精确类型:

    double val = 3.9 - (3.9 % 0.1);
    System.out.println(val); // 3.8000000000000003

    BigDecimal x = new BigDecimal( "3.9" );
    BigDecimal bdVal = x.subtract( x.remainder( new BigDecimal( "0.1" ) ) );
    System.out.println(bdVal); // 3.9

为什么是3.8000...003?因为Java使用FPU来计算结果。在IEEE双精度表示法中,3.9无法准确存储,所以它会存储3.89999...而不是3.9。而3.8999%0.01的结果为0.09999...因此结果比3.8略大一点。


1
+1 我在电气工程课上讨厌手算 IEEE 浮点数,但这是值得的。 - Denis Tulskiy
好的回答。那么为什么FPU能够计算出((3.9D / 0.1D) % 1D) = 0.0(基本上是稍微重新组织了一下同样的问题),而由于舍入误差,它认为(3.9D % 0.1D) = 0.0999999999999997 - Dave Johnson
@DaveJohnson:这不好说。当你这样做时,舍入误差可能会对你有利(即模块创建的分数太小)。 - Aaron Digulla
1
如我在我的回答中所解释的那样:它的效果不是很好。一些舍入误差会从“double”泄漏到“BigDecimal”。此外:您经常从输入端(文本文件、UI输入字段)获取字符串,并在创建“BigDecimal”之前将它们转换为“double”是浪费时间和(小)风险。 - Aaron Digulla
1
@bcsb1001:在OpenJDK 8u91中,BigDecimal#valueOf(double)的实现方式是new BigDecimal(Double.toString(x))。原因显然是Java语言规范要求浮点数转字符串的精度比IEEE对于一般浮点数运算的要求更高。 - David Foerster

5

来自Java语言规范:

%操作符计算的浮点数余数结果与IEEE 754定义的余数操作结果不同。IEEE 754余数操作通过四舍五入除法计算余数,而不是截断除法,因此其行为不能类比于通常的整数余数操作。相反,Java编程语言将%用于浮点运算,其行为类似于整数余数操作符;这可与C库函数fmod进行比较。 IEEE 754余数操作可以由库例程Math.IEEEremainder计算。

换句话说,这是因为Java对计算余数所涉及的除法结果四舍五入,而IEEE754则指定截断除法的答案。这个特定的情况似乎非常清楚地展示了这种差异。

您可以使用Math.IEEEremainder获得您期望的答案:

System.out.println(3.9 - (3.9 % 0.1));
System.out.println(3.9 - Math.IEEEremainder(3.9, 0.1));

2

1

如果您知道要处理的小数位数,可以尝试先转换为整数。这只是浮点不精确的经典案例。您所做的不是 3.9 % 0.1,而是类似于 3.899 % 0.0999


1
谷歌将声称3.9%0.1 = -4.4408921×10-16,这也不太符合模运算。 - Stroboskop

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