Java取模运算符得到错误的结果?

4

我猜这个问题以前已经被问过了,但似乎找不到匹配的问题......

我正在使用Scala,但我很确定这只是一个Java问题... 输入值是双精度浮点数。

println(28.0 / 5.6) 
println(28.0 % 5.6)

这几行的结果是:
5.0 
1.7763568394002505E-15 

这意味着Java在执行除法时是正确的,但由于某种原因获取了错误的模数,因为对于任何解决为整数的除法问题,模数应为0...

有没有解决方法?

谢谢!


2
你在做浮点数运算。除法不会给你“5”,而是给你“类似于5.0”的结果。这里没有无限的精度。取模运算会给你余数,它也受到同样的精度问题影响,并且会给你接近0的结果。在你拥有的精度范围内,这是非常接近答案的。唯一的解决方法是:如果你想得到精确的答案,就不要进行浮点数运算。 - Nanne
顺便说一句:C# 对于双精度浮点数也会产生相同的结果,因此这显然不仅仅是 Java 的问题。 - StriplingWarrior
4
这句话的意思是:“这并不是Java或C#存在的问题,而是关于原帖作者对结果的理解上存在问题 :)”。” - Jon Skeet
2
@Sam:你不是在处理十进制数学系统,也不是在处理56——你正在处理“最接近5.6的双精度浮点数”,这确实非常不同。如果你继续试图坚持你对浮点运算应该如何工作的看法,那么你会感到失望的。更有成效的做法是学习二进制浮点数的实际工作原理,了解应该避免什么以及替代方案是什么(例如Java中的BigDecimal)。 - Jon Skeet
1
@Sam:但是double根本不是十进制数学系统,而是二进制数学系统。一旦你知道了这一点,其他的就很容易了。如果你使用正确的类型,在Java中也不需要考虑这一点;但我认为合理地期望软件工程师了解所使用语言中类型的基础知识(而不是细节)。重要的是要明白,double不是十进制浮点类型(例如C#中的decimal)。对我来说,这就像知道String由字符而不是字节组成以及为什么这很重要。 - Jon Skeet
显示剩余6条评论
4个回答

13

5.0的意思是,Java理解的精确结果比任何其他double更接近5.0。这并不意味着操作的精确结果是完全等于5.0。

当您请求模数时,可以对细节进行更精细的调整,因为结果不被固定为具有“5”部分。

这不是一个很好的解释,但是想象一下你有一个带有4位精度的小数浮点类型。1000/99.99和1000%99.99的结果是什么?

实际结果以10.001001开头 - 因此您必须将其四舍五入为10.00。然而,余数为0.10,您可以表示。所以再次,看起来除法给您整数,但它并不完全是整数。

请记住,您的字面值为5.6,实际上是5.5999999999999996447286321199499070644378662109375。现在显然28.0(可以)被准确表示为该数字的商,但并非完全等于5。

编辑:如果使用BigDecimal执行具有十进制浮点算术,则该值确实完全等于5.6,并且没有问题:

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal x = new BigDecimal("28.0");
        BigDecimal y = new BigDecimal("5.6");

        BigDecimal div = x.divide(y);
        BigDecimal rem = x.remainder(y);

        System.out.println(div); // Prints 5
        System.out.println(rem); // Prints 0.0
    }
}

1
更准确地说,它比println的默认浮点精度中可以表示的任何其他双倍数更接近5.0。 - Russell Borogove
1
@Nayuki:不,5.0是“与任何其他double类型的结果一样接近于操作的理论结果”。我没有说5.0不是确切的5——我说精确结果不是5。看看我编辑后答案的底部,你就会明白我的意思了…… - Jon Skeet
1
@Nayuki Minase:在计算本身中,存在轻微的浮点精度问题,但是当您将计算出的值表示为具有整数值的Double时,它失去了保留此不精确性所必需的精度(很奇怪,对吧?)。 Double没有足够的位来表示5.000000000000001,因此它将变成5.0。但是,当计算结果不再包括“5”时,就会出现0.000000000000001。现在,double能够表示这一点,因为它不必表示5和1之间的所有数字。 - StriplingWarrior

3
实际结果并非为0,但非常接近(0.00000000000000177635...)。问题在于有些十进制数无法用二进制精确表示,因此这就是问题所在;我会猜测C/C++也会打印出相同的结果。

2
C/C++通过禁止floatdouble作为模运算符的参数来解决这个问题 :) - QuantumMechanic
啊,没意识到。我从来没有实际需要这样做过。 - rm5248
1
虽然C/C++没有适用于浮点类型的模运算符,但它们有一个等效的函数,double使用remainder(),float使用remainderf()。对于这种情况,双精度版本的输出与Java中Sam Dealey所看到的匹配:x=2.8000000000000000e+001(403c000000000000),y=5.5999999999999996e+000(4016666666666666),res=1.7763568394002505e-015(3ce0000000000000)。正如其他帖子中指出的那样,问题在于5.6在双精度浮点数中无法准确表示。remainder()本身的结果始终是精确的。 - njuffa

2

算术

首先,十进制数5.6无法在二进制浮点数中被精确表示。它被舍入为精确的二进制分数3152519739159347/249

28.0 / 5.6 = 5.0的原因是因为5.0是最接近真实结果的double数字,而真实结果是5.0000000000000003172065784643....

至于28.0%5.6,真实结果恰好为1/249,约为1.776×10−15,因此计算是正确舍入的。

解决方法

为什么需要解决方法?对于大多数应用程序来说,保留非常微小的错误结果就足够了。您是否关心显示“漂亮”的结果?

如果您需要绝对精确的算术运算,则需要使用BigFraction的某些实现。

更多阅读

浮点数注意事项的主题已被涵盖在各种文章中:

(按读者友好性递减的顺序。)


我正在为艺术应用程序创建一个标尺,并使用模数来确定数组中的索引,以查找标尺上标记的高度--如果它没有找到模值为零的高度,则返回-1并产生ArrayIndexOutOfBounds错误,所以,是的,这不是“漂亮”的问题,而是功能性的。 - Sam Dealey
1
你能否重写你的数学代码,使其只进行整数运算?例如,不使用 28 % 5.6,而是将所有数字左移一位,然后计算 280 % 56? - Nayuki

0

这是我使用模运算符检查双精度值是否可被另一个数整除的解决方案:

public class DoubleOperation
{
    public static final double EPSILON = 0.000001d;

    public static boolean equals(double val1, double val2)
    {
        return (Math.abs(val1 - val2) < EPSILON);
    }

    public static boolean divisible(double dividend, double divisor)
    {
        double divisionRemainder = dividend % divisor;
        return (equals(divisionRemainder, 0.0d) || equals(divisionRemainder, divisor));
    }
}

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