Почему Math.round (0.49999999999999994) возвращает 1? - PullRequest
554 голосов
/ 28 марта 2012

В следующей программе вы можете видеть, что каждое значение чуть меньше .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.

Ответы [ 5 ]

564 голосов
/ 28 марта 2012

Резюме

В 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. 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

232 голосов
/ 28 марта 2012

Это, кажется, известная ошибка ( Ошибка Java 6430675: Math.round имеет удивительное поведение для 0x1.fffffffffffffp-2 ), которая была исправлена ​​в Java 7.

83 голосов
/ 28 марта 2012

Исходный код в 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.

26 голосов
/ 28 марта 2012

У меня то же самое на 32-битной JDK 1.6, но на 64-битной Java 7 у меня 0 для 0.49999999999999994, округленное 0, и последняя строка не печатается. Кажется, это проблема виртуальной машины, однако при использовании чисел с плавающей запятой следует ожидать, что результаты будут немного отличаться в разных средах (процессор, 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
11 голосов
/ 05 июня 2013

Ответ, приведенный ниже, является выдержкой из сообщения об ошибке Oracle 6430675 at. Посетите отчет для полного объяснения.

Методы {Math, StrictMath.round оперативно определены как

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

для двойных аргументов. Хотя это определение обычно работает как положено, оно дает удивительный результат 1, а не 0, для 0x1.fffffffffffffp-2 (0.49999999999999994).

Значение 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. В арифметическом округлении IEEE 754 до ближайшего четного режима округления, используемого Java, когда результаты с плавающей запятой неточны, чем ближе эти два представимые значения с плавающей точкой, которые заключают в скобки точный результат, должны быть возвращены; если оба значения в равной степени близки, возвращается то, которое его последний бит ноль. В этом случае правильное возвращаемое значение из сложения равно 1, а не наибольшему значению меньше 1.

Пока метод работает, как определено, поведение на этом входе очень удивительно; спецификация может быть изменена на что-то более похожее на «Округление до ближайшего длинного, округление связывания», что позволит изменить поведение на этом входе.

...