5

Почему Math.round(0.49999999999999994) возвращает 1?

17

Проблема с округлением чисел с плавающей точкой в Java

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

Обратите внимание, что числа, находящиеся вблизи 0.5, округляются вниз, за исключением 0.5, которое округляется вверх. Это вызывает вопросы и недоумение, особенно учитывая, что в других языках программирования такое поведение может быть другим.

Я использую Java 6, обновление 31. Кто-нибудь может объяснить, что происходит в этом случае, и предложить решение или обходной путь для корректного округления?

3 ответ(ов)

5

Резюме

В Java 6 (и, вероятно, в предыдущих версиях) метод round(x) реализован как floor(x + 0.5). Это ошибка спецификации, касающаяся исключительно одного патологического случая. В Java 7 больше не требуется эта проблемная реализация.

Проблема

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 (например) реализует метод round следующим образом:

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) // максимальное значение double меньше 0.5
        return (long) floor(a + 0.5d);
    else
        return 0;
}

0

В JDK 6 код для округления числа выглядит следующим образом:

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

Здесь, если a равно 0.49999999999999994d, вызовется метод Math.floor, и результатом будет 1, так как 0.49999999999999994 + 0.5 дает 0.99999999999999994, а его округление вниз — 0.

Однако в JDK 7 функция была изменена:

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) {
        // a не является наибольшим значением double меньше 0.5
        return (long)Math.floor(a + 0.5d);
    } else {
        return 0;
    }
}

Теперь добавлена проверка: если a не равно наибольшему значению double, меньшему 0.5 (что соответствует 0.49999999999999994 в бинарном представлении), выполняется округление, как и в JDK 6. Но если a равно этому значению, то функция вернёт 0.

Таким образом, когда вы проверяете значение 0.49999999999999994d, в JDK 6 результат будет 1, в то время как в JDK 7, из-за добавленной проверки, функция вернёт 0.

Если же вы попробуете значение 0.49999999999999999d, результат будет 1, потому что это максимальное значение double, меньшее 0.5.

0

Проблема, с которой вы столкнулись, может быть связана с особенностями представления чисел с плавающей запятой в Java, особенно между 32-битными и 64-битными версиями JVM. На JDK 1.6 32-бит вы получаете результаты, которые отличаются от результатов на Java 7 64-бит, где 0.49999999999999994 округляется до 0, и последняя строка не выводится. Это может указывать на проблему совместимости виртуальной машины, однако стоит помнить, что при работе с числами с плавающей запятой результаты могут варьироваться в зависимости от окружения (процессор, 32- или 64-битный режим).

Вот пример вывода для x64:

10.5 округлено в 11
10.499999999999998 округлено в 10
9.5 округлено в 10
9.499999999999998 округлено в 9
8.5 округлено в 9
8.499999999999998 округлено в 8
7.5 округлено в 8
7.499999999999999 округлено в 7
6.5 округлено в 7
6.499999999999999 округлено в 6
5.5 округлено в 6
5.499999999999999 округлено в 5
4.5 округлено в 5
4.499999999999999 округлено в 4
3.5 округлено в 4
3.4999999999999996 округлено в 3
2.5 округлено в 3
2.4999999999999996 округлено в 2
1.5 округлено в 2
1.4999999999999998 округлено в 1
0.5 округлено в 1
0.49999999999999994 округлено в 0

Такие небольшие расхождения в значениях действительно могут существенно повлиять на результаты, особенно когда вы используете функции округления или производите операции с матрицами. Это следует учитывать при разработке приложений, использующих числа с плавающей запятой.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь