为什么Math.round(0.49999999999999994)返回1?

590
在下面的程序中,你可以看到每个值略小于.5都会被向下舍入,除了0.5
for (int i = 10; i >= 0; i--) {
    long l = Double.doubleToLongBits(i + 0.5);
    double x;
    do {
        x = Double.longBitsToDouble(l);
        System.out.println(x + " rounded is " + Math.round(x));
        l--;
    } while (Math.round(x) > i);
}

打印

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 1
0.4999999999999999 rounded is 0

我正在使用Java 6更新31。


1
在Java 1.7.0上它可以正常工作 http://i.imgur.com/hZeqx.png - Caffeinated
2
@Adel:请看我在Oli的回答中的评论,看起来Java 6实现了这个功能(并且文档也说明了),但是它的实现方式可能会通过添加0.5到数字然后使用floor导致进一步的精度损失;Java 7 不再以这种方式记录(可能/希望是因为他们修复了它)。 - T.J. Crowder
1
这是我写的一个测试程序中的一个bug。 ;) - Peter Lawrey
1
另一个例子表明浮点数值不能轻信。 - Michaël Roy
1
经过思考,我认为没有问题。0.49999999999999994比小于0.5的最小可表示数字要大,并且在十进制人类可读形式中的表示本身就是一个近似值,试图欺骗我们。 - Michaël Roy
显示剩余6条评论
5个回答

593

摘要

在Java 6(和可能更早的版本)中,round(x)的实现是 floor(x+0.5).1 这是一个规范错误,仅存在于这个极端情况下.2 Java 7不再强制使用这个有问题的实现.3

问题

0.5+0.49999999999999994在双精度中恰好等于1:

static void print(double d) {
    System.out.printf("%016x\n", Double.doubleToLongBits(d));
}

public static void main(String args[]) {
    double a = 0.5;
    double b = 0.49999999999999994;

    print(a);      // 3fe0000000000000
    print(b);      // 3fdfffffffffffff
    print(a+b);    // 3ff0000000000000
    print(1.0);    // 3ff0000000000000
}

这是因为0.49999999999999994的指数比0.5小,所以当它们相加时,它的尾数被移位,ULP变大。

解决方案

自Java 7以来,OpenJDK(例如)实现如下:4

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) // greatest double value less than 0.5
        return (long)floor(a + 0.5d);
    else
        return 0;
}

1. http://docs.oracle.com/javase/6/docs/api/java/lang/Math.html#round%28double%29
2. https://bugs.java.com/bugdatabase/view_bug?bug_id=6430675 (感谢@SimonNickerson发现此问题)
3. http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round%28double%29
4. http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/java/lang/Math.java#Math.round%28double%29

3
哦,这很有趣,他们在Java 7中删除了那部分内容(我提供的文档),也许是为了避免触发(进一步)精度损失而导致这种奇怪的行为。 - T.J. Crowder
6
请看例如http://en.wikipedia.org/wiki/Single_precision和http://en.wikipedia.org/wiki/Unit_in_the_last_place。这些页面介绍了单精度浮点数和最后一位单位。 - Oliver Charlesworth
2
我不禁认为这个修复只是表面性的,因为0是最显眼的。毫无疑问,许多其他浮点数值都会受到这种舍入误差的影响。 - Michaël Roy
3
实际上并不是所有数字,只有一个特定的数字。没有其他数字比 n + 1/2 更小但又足够接近 n + 1/2,以至于加上 x + 1/2 能得到 n + 1,而不是更小的值。 - gnasher729
1
@gnasher 我不同意。 四舍五入是与领域相关的原因。 对于科学应用和金融应用,您不会以相同的方式四舍五入数字,正是出于这个原因。 例如:在图形或DSP中使用除floor(x + .5f)之外的任何内容都将不可避免地导致灾难。 这可能也是为什么round()从未在科学应用程序中使用的原因。 - Michaël Roy
显示剩余7条评论

241

6
+1:很棒的发现!与我回答中解释的Java 6和7文档差异相吻合。 - Oliver Charlesworth
2
实现round()函数比大多数人想象的要难得多。 - Shafik Yaghmour
@ShafikYaghmour 关于这个问题,x86只在4种IEEE 754-1985舍入模式(vroundsd指令)中实现了向整数舍入,但不幸的是,这意味着对于最接近的舍入,它是按照规定的round()而言的“就近偶数舍入”,而不是“就近远离”。在硬件中增加“就近远离”可能几乎没有成本,尤其当已经有其他舍入模式时。我不清楚其他体系结构是否存在类似情况。 - vinc17

87

JDK 6 中的源代码:

public static long round(double a) {
    return (long)Math.floor(a + 0.5d);
}

JDK 7 中的源代码:

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) {
        // a is not the greatest double value less than 0.5
        return (long)Math.floor(a + 0.5d);
    } else {
        return 0;
    }
}
当值为0.49999999999999994d时,在JDK 6中,它会调用 floor 函数并返回1,但在JDK 7中,if条件检查的是该数字是否为小于0.5的最大双精度值。由于此情况下该数字不是小于0.5的最大双精度值,因此 else 块将返回 0。

您可以尝试0.49999999999999999d,它将返回1而不是0,因为这是小于0.5的最大双精度值。

这里的1.499999999999999994会发生什么?返回2吗?它应该返回1,但这应该会让你遇到与之前相同的错误,只不过是1的情况。 - mjs
7
双精度浮点数无法表示 1.499999999999999994。而 1.4999999999999998 是小于 1.5 的最小双精度浮点数。从问题中可以看出,使用“floor”方法可以正确四舍五入。 - OrangeDog

28

我在JDK 1.6 32位上也遇到了同样的问题,但是在Java 7 64位上,对于0.49999999999999994的结果是0,且最后一行没有被打印。这似乎是一个虚拟机问题,然而使用浮点数,你应该预计在不同的环境(CPU、32位或64位模式)中结果会略有差异。

而且,当使用round或翻转矩阵等操作时,这些可能会产生巨大的影响。

x64输出:

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 0

在Java 7中(您用于测试的版本),该错误已经修复。 - Iván Pérez
1
我想你的意思是32位。我怀疑http://en.wikipedia.org/wiki/ZEBRA_%28computer%29能否运行Java,而且我怀疑自那以后是否有33位机器。 - chx
@chx 很明显,因为我之前写过32位的代码 :) - Danubian Sailor

15
以下是一个Oraclebug报告6430675的摘录。请访问报告以获取完整的解释。 方法{Math, StrictMath.round的操作定义如下:
(long)Math.floor(a + 0.5d)

对于双精度参数。虽然这个定义通常按预期工作,但对于0x1.fffffffffffffp-2(0.49999999999999994),它给出了令人惊讶的结果1,而不是0。
值0.49999999999999994是小于0.5的最大浮点值。作为十六进制浮点文字,它的值是0x1.fffffffffffffp-2,等于(2 - 2^52)* 2^-2. ==(0.5 - 2^54)。因此,这个和的确切值是
(0.5 - 2^54) + 0.5

1 - 2^54是一个介于两个相邻的浮点数(1 - 2^53和1)之间的值。在Java中使用的IEEE 754算术最接近偶数舍入模式中,当浮点结果不精确时,必须返回与精确结果夹在中间的两个可表示浮点数中更接近的那个;如果两个值都同样接近,那么返回最后一位为零的那个值。在这种情况下,从加法中正确返回的值是1,而不是小于1的最大值。
虽然该方法的操作符合定义,但在这种输入情况下的行为非常令人惊讶;规范可以修改为更接近于“四舍五入到最接近的长整型,如果有多个值相等,则向上舍入”,这样可以改变该输入的行为。

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