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


567

在下面的程序中,您可以看到.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 update 31。


1
在Java 1.7.0上可以正常使用i.imgur.com/hZeqx.png –'Coffee
Coffee

2
@Adel:请参阅我对Oli答案的评论,看起来Java 6实现了此方法(以及它所做的文档)的方式可能是:将0.5数字加到然后使用floor,会进一步导致精度损失。Java 7 不再以这种方式进行记录(大概/希望是因为他们已将其修复)。
TJ Crowder 2012年

1
这是我编写的测试程序中的错误。;)
彼得·劳瑞

1
另一个显示浮点值的示例不能取为面值。
米切尔·罗伊

1
经过考虑。我没问题。0.49999999999999994大于小于0.5的最小可表示数字,而人类可以理解的十进制形式的表示本身就是试图欺骗我们的近似值
米切尔·罗伊

Answers:


574

摘要

在Java 6(可能更早)中,round(x)实现为floor(x+0.5)1 这是一个规范错误,恰好是这种病理情况。2 Java 7不再强制执行此无效的实现。3

问题

0.5 + 0.499999999999999999994的双精度正好为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. http://bugs.java.com/bugdatabase/view_bug.do?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


我没有roundJavadocMath.roundMath课程概述中看到该定义。
TJ Crowder 2012年

3
@ Oli:哦,现在这很有趣,他们花了点钱在Java 7(我链接到的文档)上-也许是为了避免由于触发(进一步)精度损失而引起这种奇怪的行为。
TJ Crowder 2012年

@TJCrowder:是的,很有趣。您是否知道针对各个Java版本的“发行说明” /“改进”文档,以便我们可以验证这一假设?
奥利弗·查尔斯沃思


1
我忍不住认为此修复程序只是表面上的,因为最明显的是零。毫无疑问,此舍入误差会影响许多其他浮点值。
米切尔·罗伊


83

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的最大double值。由于在这种情况下,该数字不是小于0.5的最大double值,因此该else块返回0。

您可以尝试0.49999999999999999d,它将返回1,但不会返回0,因为这是小于0.5的最大double值。


1.499999999999999994在这里会发生什么?返回2?它应该返回1,但这将使您获得与之前相同的错误,但返回1。
毫米

6
1.499999999999999994不能用双精度浮点表示。1.4999999999999998是小于1.5的最小两倍。从问题中可以看出,该floor方法将其正确舍入。
OrangeDog 2012年

26

我在32位的JDK 1.6上具有相同的功能,但是在Java 7 64位的上,对于0.49999999999999994我有0,其四舍五入为0,并且不打印最后一行。这似乎是VM的问题,但是,使用浮点运算时,您应该期望在各种环境(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(用于测试它的版本)中,该错误已修复。
伊万·佩雷斯

1
我认为您的意思是32位。我怀疑en.wikipedia.org/wiki/ZEBRA_%28computer%29可以运行Java,并且我怀疑此后是否有33位计算机。
chx 2015年

@chx很明显,因为我之前写过32位:)
Danubian Sailor

11

此后的答案是Oracle 错误报告6430675的摘录。访问报告以获取完整说明。

方法{Math,StrictMath.round在操作上定义为

(long)Math.floor(a + 0.5d)

对于双重论点。尽管此定义通常可以按预期工作,但对于0x1.fffffffffffffffp-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的最大值。

当该方法按定义操作时,此输入上的行为非常令人惊讶。可以将规范修改为类似于“舍入到最接近的长整数,向上舍入”,这将允许更改此输入的行为。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.