Статьи

Почему Math.round (0.499999999999999917) округляется до 1 на Java 6

обзор

Существует два типа ошибок представления ошибок и ошибок арифметического округления, которые являются общими в вычислениях с плавающей запятой. Эти две ошибки объединяются в этом простом примере, Math.round (0.499999999999999917) округляется до 1 в Java 6.

Ошибка представления

Плавающая точка — это базовый формат 2, что означает, что все числа представлены в виде суммы степеней 2. Например, 6.25 — это 2 ^ 2 + 2 ^ 1 + 2 ^ -2. Однако даже простые числа, такие как 0,1, не могут быть представлены точно. Это становится очевидным при преобразовании в BigDecimal, поскольку оно сохранит значение, фактически представленное без округления.

1
2
3
new BigDecimal(0.1)=
    0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(0.1)= 0.1

Используя конструктор, вы получаете фактически представленное значение, а использование valueOf дает такое же округленное значение, которое вы увидели бы, если бы вы напечатали двойное

Когда число анализируется, оно округляется до ближайшего представленного значения. Это означает, что есть число немного меньше 0,5, которое будет округлено до 0,5, потому что это самое близкое представленное значение.

Следующее выполняет поиск методом перебора для наименьшего значения, округленное становится 1,0

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static final BigDecimal TWO = BigDecimal.valueOf(2);
 
public static void main(String... args) {
    int digits = 80;
 
    BigDecimal low = BigDecimal.ZERO;
    BigDecimal high = BigDecimal.ONE;
 
    for (int i = 0; i <= 10 * digits / 3; i++) {
        BigDecimal mid = low.add(high).divide(TWO, digits, RoundingMode.HALF_UP);
        if (mid.equals(low) || mid.equals(high))
            break;
        if (Math.round(Double.parseDouble(mid.toString())) > 0)
            high = mid;
        else
            low = mid;
    }
 
    System.out.println("Math.round(" + low + ") is " +
            Math.round(Double.parseDouble(low.toString())));
    System.out.println("Math.round(" + high + ") is " +
            Math.round(Double.parseDouble(high.toString())));
}

Исходный код

На Java 7 вы получите следующий результат.

1
2
Math.round(0.49999999999999997224442438437108648940920829772949218749999999999999999999999999) is 0
Math.round(0.49999999999999997224442438437108648940920829772949218750000000000000000000000000) is 1

Что удивительно, так это то, что в Java 6 вы получаете следующее.

1
2
Math.round(0.49999999999999991673327315311325946822762489318847656250000000000000000000000000) is 0
Math.round(0.49999999999999991673327315311325946822762489318847656250000000000000000000000001) is 1

Откуда эти цифры?

Значение Java 7 — это средняя точка между 0,5 и предыдущим значением представления. Выше этой средней точки значение округляется до 0,5 при анализе.

Значение Java 6 является средней точкой между значением value до 0.5 и значением до этого.

01
02
03
04
05
06
07
08
09
10
11
Value 0.5 is 0.5
The previous value is 0.499999999999999944488848768742172978818416595458984375
... and the previous is 0.49999999999999988897769753748434595763683319091796875
 
The mid point between 0.5
 and 0.499999999999999944488848768742172978818416595458984375
 is 0.4999999999999999722444243843710864894092082977294921875
 
... and the mid point between 0.499999999999999944488848768742172978818416595458984375
 and 0.49999999999999988897769753748434595763683319091796875
 is 0.4999999999999999167332731531132594682276248931884765625

Почему значение Java 6 меньше

В Java 6 Javadoc Math.round (double) определяется как

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

Проблема с этим определением заключается в том, что 0,49999999999999994 + 0,5 имеет ошибку округления, которая приводит к значению 1,0.

В Java 7 Javadoc Math.round (double) просто говорится:

Возвращает ближайшую длинную к аргументу связь с округлением в большую сторону.

Так как же это исправить в Java 7?

Исходный код для Java 7 Math.round выглядит так

1
2
3
4
5
6
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;
}

Результат для наибольшего значения менее 0,5 жестко закодирован.

Так что же такое 0x1.ffffffffffffp-2?

Это шестнадцатеричное представление значения с плавающей запятой. Он используется редко, но он точен, так как все значения могут быть представлены без ошибок (до 53 бит).

Ссылки по теме

Ошибка ID: 6430675 Math.round имеет удивительное поведение для 0x1.fffffffffffffp-2
Почему Math.round (0.49999999999999994) возвращает 1

Ссылка: почему Math.round (0.499999999999999917) округляется до 1 на Java 6 от нашего партнера по JCG Питера Лоури из блога Vanilla Java .