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

在下面的程序中,您可以看到,对于每个稍微小于.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。

概要

在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变大。

解决scheme

从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 (信贷给@SimonNickersonfind这个)

http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round%28double%29

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/java/lang/Math.java#Math.round%28double%29

这似乎是一个已知的bug( Java bug 6430675:Math.round对于0x1.fffffffffffffp-2具有令人惊讶的行为 ),这已经在Java 7中修复了。

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条件是检查该数是否是最大double值小于0.5。 在这种情况下,数字不是小于0.5的最大double值,所以else块返回0。

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

我已经在jdk 1.6 32bit上得到了相同的结果,但是在java 7 64bit上,我已经去掉0.49999999999999994的值了,舍入为0,最后一行不打印。 这似乎是虚拟机问题,但是,使用浮点数,您应该期望在各种环境(CPU,32位或64位模式)上的结果有所不同,

而且,当使用round或反转matrix等时,这些可以产生巨大的差异。

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 

以下答案是http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6430675上的一个oracle错误报告的摘录。; 访问链接的完整说明。

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

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

双重论点。 虽然这个定义通常按照预期工作,但是对于0x1.fffffffffffffp-2(0.49999999999999994),却给出了令人惊讶的结果1,而不是0。

值0.49999999999999994是小于0.5的最大浮点值。 作为hex浮点数字,其值为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之间的一半。在IEEE 754algorithm轮到Java使用的最接近的舍入模式中,当浮点结果不精确时,必须返回包含确切结果的可表示的浮点值; 如果两个值相等,则返回最后一位为零的那个值。 在这种情况下,来自add的正确返回值是1,而不是小于1的最大值。

虽然该方法按照定义运行,但这种input的行为是非常令人惊讶的。 这个规范可以被定义为更接近于“最接近最长,四舍五入关系”的东西,这样就可以改变这个input的行为。