Подтвердить что ты не робот

Ошибка округления NumberFormat только с Java 8

Может кто-нибудь объяснить мне, почему следующий код:

public class Test {
    public static void main(String... args) {
        round(6.2088, 3);
        round(6.2089, 3);
    }

    private static void round(Double num, int numDecimal) {
        System.out.println("BigDecimal: " + new BigDecimal(num).toString());

        // Use Locale.ENGLISH for '.' as decimal separator
        NumberFormat nf = NumberFormat.getInstance(Locale.ENGLISH);
        nf.setGroupingUsed(false);
        nf.setMaximumFractionDigits(numDecimal);
        nf.setRoundingMode(RoundingMode.HALF_UP);

        if(Math.abs(num) - Math.abs(num.intValue()) != 0){
            nf.setMinimumFractionDigits(numDecimal);
        }

        System.out.println("Formatted: " + nf.format(num));
    }
}

дает следующий вывод:

[[email protected] trunk]$ java Test
BigDecimal: 6.208800000000000096633812063373625278472900390625
Formatted: 6.209
BigDecimal: 6.208899999999999863575794734060764312744140625
Formatted: 6.208

Если вы этого не видите: "6.2089" округляется до 3 цифр, выдает результат "6.208", а "6.2088" - "6.209". Меньше больше?

Результаты были хорошими при использовании Java 5, 6 или 7, но этот Java 8 дает мне этот странный результат. Версия Java:

[[email protected] trunk]$ java -version
java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) Server VM (build 25.5-b02, mixed mode)

EDIT: это выход Java 7:

[[email protected] trunk]$ java Test
BigDecimal: 6.208800000000000096633812063373625278472900390625
Formatted: 6.209
BigDecimal: 6.208899999999999863575794734060764312744140625
Formatted: 6.209

Версия Java 7:

[[email protected] trunk]$ java -version
java version "1.7.0_51"
Java(TM) SE Runtime Environment (build 1.7.0_51-b13)
Java HotSpot(TM) Server VM (build 24.51-b03, mixed mode)
4b9b3361

Ответ 1

Я мог бы отследить эту проблему до класса java.text.DigitList строки 522.

Ситуация состоит в том, что он считает, что десятичные цифры 6.0289 уже округлены (что верно при сравнении с эквивалентным представлением BigDecimal 6.208899…) и решает не округлять снова. Проблема в том, что это решение имеет смысл только в том случае, если цифра, полученная в результате округления, составляет 5, а не тогда, когда она больше, чем 5. Обратите внимание на то, как код HALF_DOWN корректно различает случай digit=='5' и digit>'5'.

Это ошибка, очевидно, и странная, учитывая тот факт, что код для аналогичного права (только для другого направления) находится прямо под сломанным.

        case HALF_UP:
            if (digits[maximumDigits] >= '5') {
                // We should not round up if the rounding digits position is
                // exactly the last index and if digits were already rounded.
                if ((maximumDigits == (count - 1)) &&
                    (alreadyRounded))
                    return false;

                // Value was exactly at or was above tie. We must round up.
                return true;
            }
            break;
        case HALF_DOWN:
            if (digits[maximumDigits] > '5') {
                return true;
            } else if (digits[maximumDigits] == '5' ) {
                if (maximumDigits == (count - 1)) {
                    // The rounding position is exactly the last index.
                    if (allDecimalDigits || alreadyRounded)
                        /* FloatingDecimal rounded up (value was below tie),
                         * or provided the exact list of digits (value was
                         * an exact tie). We should not round up, following
                         * the HALF_DOWN rounding rule.
                         */
                        return false;
                    else
                        // Value was above the tie, we must round up.
                        return true;
                }

                // We must round up if it gives a non null digit after '5'.
                for (int i=maximumDigits+1; i<count; ++i) {
                    if (digits[i] != '0') {
                        return true;
                    }
                }
            }
            break;

Причина, по которой это не происходит с другим числом, заключается в том, что 6.2088 не является результатом округления (опять же, сравните с выходом BigDecimal 6.208800…). Таким образом, в этом случае он будет округлен.

Ответ 2

Oracle исправила эту ошибку в обновлении Java 8 40

Неофициальный патч для выполнения доступен для более ранних версий

Благодаря результатам исследования ответа Holger мне удалось разработать патч для выполнения, и мой работодатель выпустил его бесплатно в соответствии с условиями лицензии GPLv2 с Classpath Exception 1 (то же, что и исходный код OpenJDK).

Патч-проект и исходный код размещены на GitHub с более подробной информацией об этой ошибке, а также ссылками на загружаемые двоичные файлы. Патч не вносит изменений в установленные файлы Java на диске и должен быть безопасным для использования во всех версиях Oracle Java >= 6 и, по крайней мере, в версии 8 (включая исправленные версии).

Когда патч обнаруживает сигнатуры байт-кода, которые предполагают наличие ошибки, он заменяет случай переключателя HALF_UP на исправленную реализацию:

if (digits[maximumDigits] > '5') {
    return true;
} else if (digits[maximumDigits] == '5') {
    return maximumDigits != (count - 1)
        || allDecimalDigits
        || !alreadyRounded;
}
// else
return false; // in original switch(), was: break;

1 Я не юрист, но я понимаю, что GPLv2 w/CPE позволяет коммерческое использование в двоичной форме без применения GPL для совместной работы.

Ответ 3

Прослеживание кода, который вы получаете в DigitList.set

final void set(boolean isNegative, double source, int maximumDigits, boolean fixedPoint) {

    FloatingDecimal.BinaryToASCIIConverter fdConverter  = FloatingDecimal.getBinaryToASCIIConverter(source);
    boolean hasBeenRoundedUp = fdConverter.digitsRoundedUp();

У меня более простой тест на эту ошибку

import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;

public class Test {
    public static void main(String... args) {
        for (int i = 0; i < 100; i++)
            test(i / 100.0);
    }

    private static void test(double num) {
        NumberFormat nf = NumberFormat.getInstance(Locale.ENGLISH);
        nf.setMaximumFractionDigits(1);
        String round1 = nf.format(num);

        NumberFormat nf2 = NumberFormat.getInstance(Locale.ENGLISH);
        nf2.setMaximumFractionDigits(1);
        nf2.setRoundingMode(RoundingMode.HALF_UP);
        String round2 = nf2.format(num);
        if (!round1.equals(round2))
            System.out.printf("%s, formatted with HALF_UP was %s but should be %s%n", num, round2, round1);
    }
}

печатает

0.06, formatted with HALF_UP was 0 but should be 0.1
0.09, formatted with HALF_UP was 0 but should be 0.1
0.18, formatted with HALF_UP was 0.1 but should be 0.2
0.25, formatted with HALF_UP was 0.3 but should be 0.2
0.29, formatted with HALF_UP was 0.2 but should be 0.3
0.36, formatted with HALF_UP was 0.3 but should be 0.4
0.37, formatted with HALF_UP was 0.3 but should be 0.4
0.47, formatted with HALF_UP was 0.4 but should be 0.5
0.48, formatted with HALF_UP was 0.4 but should be 0.5
0.49, formatted with HALF_UP was 0.4 but should be 0.5
0.57, formatted with HALF_UP was 0.5 but should be 0.6
0.58, formatted with HALF_UP was 0.5 but should be 0.6
0.59, formatted with HALF_UP was 0.5 but should be 0.6
0.69, formatted with HALF_UP was 0.6 but should be 0.7
0.86, formatted with HALF_UP was 0.8 but should be 0.9
0.87, formatted with HALF_UP was 0.8 but should be 0.9
0.96, formatted with HALF_UP was 0.9 but should be 1
0.97, formatted with HALF_UP was 0.9 but should be 1
0.98, formatted with HALF_UP was 0.9 but should be 1
0.99, formatted with HALF_UP was 0.9 but should be 1

В неправильном случае hasBeenRoundedUp истинно, и это предотвращает дальнейшее округление. Обратите внимание, что если вы отбрасываете настройку округления, у него есть путь по умолчанию, который округляется правильно.

Я бы не использовал NumberFormat. Это довольно медленно и сложно использовать.

import java.math.BigDecimal;

public class Test {
    public static void main(String... args) {
        round(6.2088, 3);
        round(6.2089, 3);
    }

    private static void round(double num, int numDecimal) {
        BigDecimal bd = new BigDecimal(num);
        BigDecimal bd2 = BigDecimal.valueOf(num);
        System.out.println("new BigDecimal: " + bd);
        System.out.println("BigDecimal.valueOf: " + bd2);
        System.out.printf("%." + numDecimal + "f%n", num);
        System.out.printf("%." + numDecimal + "f%n", bd);
        System.out.printf("%." + numDecimal + "f%n", bd2);
        System.out.printf("%f%n", round3(num));
        System.out.printf("%s%n", round3(num));
        System.out.printf("%f%n", bd.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
        System.out.printf("%s%n", bd.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
        System.out.printf("%f%n", bd2.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
        System.out.printf("%s%n", bd2.setScale(numDecimal, BigDecimal.ROUND_HALF_UP));
    }

    private static double round3(double num) {
        final double factor = 1e3;
        return Math.round(num * factor) / factor;
    }
}

печатает с Java 8.

new BigDecimal: 6.208800000000000096633812063373625278472900390625
BigDecimal.valueOf: 6.2088
6.209
6.209
6.209
6.209000
6.209
6.209000
6.209
6.209000
6.209
new BigDecimal: 6.208899999999999863575794734060764312744140625
BigDecimal.valueOf: 6.2089
6.209
6.209
6.209
6.209000
6.209
6.209000
6.209
6.209000
6.209