DecimalFormat с использованием неправильного RoundingMode, даже если он установлен явно

Почему это DecimalFormat не округляется с использованием RoundingMode.HALF_UP, как ожидалось, и что нужно сделать, чтобы получить ожидаемый результат? Можно ли это сделать с помощью DecimalFormat?

DecimalFormat df = new DecimalFormat("0.0");

df.setRoundingMode(RoundingMode.HALF_UP);

df.format(0.05); // expecting 0.1, getting 0.1
df.format(0.15); // expecting 0.2, getting 0.1  << unexpected

df.format(0.06); // expecting 0.1, getting 0.0  << unexpected

Я видел ответы на этот вопрос, в частности этот ответ, но кажется, что он работает только при округлении до целого числа.

Мой JDK — 8.0.110, а JRE — 8.0.730.2.


person 4castle    schedule 13.05.2016    source источник
comment
это таинственно!   -  person tofutim    schedule 13.05.2016
comment
возможно, вам придется умножить на 10 и снова разделить в качестве обходного пути   -  person tofutim    schedule 13.05.2016
comment
Отлично работает в 1.7, возможно, см. stackoverflow.com/questions/22797964/   -  person Scary Wombat    schedule 13.05.2016
comment
интересное обсуждение здесь - stackoverflow.com/questions/ 7124448/ - этот двойной неточный   -  person tofutim    schedule 13.05.2016
comment
@ScaryWombat Я использую Java 8. Возможно, это так. Я все еще хотел бы знать, как получить ожидаемый результат.   -  person 4castle    schedule 13.05.2016
comment
@4castle, я получаю 0,1 вместо 0,06 в Java7   -  person tofutim    schedule 13.05.2016
comment
@ScaryWombat: обратите внимание, что, как указано в ответе, который вы указали, поведение Java 7, которое вы видите, неправильное.   -  person user2357112 supports Monica    schedule 13.05.2016
comment
comment
Хотя для первых двух примеров есть объяснения, последний пример (0,06 дает 0,0) действительно неожиданный; на самом деле я не могу воспроизвести его в Java 8.   -  person Henry    schedule 13.05.2016
comment
@ Генри Да, я получаю 0.0 только тогда, когда у меня включен HALF_UP. Это очень странно.   -  person 4castle    schedule 13.05.2016
comment
Может ли это быть эффект зависимости от локали? Какую локаль вы используете?   -  person Henry    schedule 13.05.2016


Ответы (3)


(Ответ ниже с использованием Java 8.)

Проблема, которую вы видите, возникает из-за указания в коде «0,15» или «0,05», что при представлении в виде двойного числа составляет чуть меньше 0,15. Проверь это

DecimalFormat df = new DecimalFormat("#.#");

df.setRoundingMode(RoundingMode.HALF_UP);

BigDecimal bd = new BigDecimal(0.15);
System.out.println("bd=" + bd);
System.out.println(df.format(0.15)); // expecting 0.1, getting 0.1
bd = new BigDecimal(0.05);
System.out.println("bd=" + bd);
System.out.println(df.format(0.05));
bd = new BigDecimal(0.06);
System.out.println("bd=" + bd);
System.out.println(df.format(0.06));

Вывод этого кода

bd=0.1499999999999999944488848768742172978818416595458984375
0.1
bd=0.05000000000000000277555756156289135105907917022705078125
0.1
bd=0.059999999999999997779553950749686919152736663818359375
0.1

Возможное решение (если вам абсолютно необходимо округлить его правильно) — использовать BigDecimal.valueOf для создания значения. Например

BigDecimal bd = BigDecimal.valueOf(0.15);
System.out.println("bd=" + bd);
System.out.println(df.format(bd)); // expecting 0.1, getting 0.1
bd = BigDecimal.valueOf(0.05);
System.out.println("bd=" + bd);
System.out.println(df.format(bd));
bd = BigDecimal.valueOf(0.06);
System.out.println("bd=" + bd);
System.out.println(df.format(bd));

Теперь будет уступать

bd=0.15
0.2
bd=0.05
0.1
bd=0.06
0.1

Кстати, как указал Scary Wombat, маска, установленная как 0,0 вместо #.#, сделает 0,6 0. Но я думаю, что это было более позднее редактирование, чем когда я начал смотреть на это. Использовать #.#.

person tofutim    schedule 13.05.2016
comment
Итак, проблема в том, что DecimalFormat df = new DecimalFormat("0.0"); против DecimalFormat df = new DecimalFormat("#.#");? - person Scary Wombat; 13.05.2016
comment
@ScaryWombat Он этого не говорил. О чем ты? - person user207421; 13.05.2016
comment
Проблема в том, что в коде пишется 0.15, что на самом деле не 0.15, оно хранится в системе как 0.1499999999999999944488848768742172978818416595458984375 - person tofutim; 13.05.2016
comment
@EJP Отсюда и моя отметка ?. ОП утверждает, что использование маски "0.0" со значением 0,06 становится 0.0, тогда как в этом ответе используется маска или "#.#", а значение становится 0.1 - person Scary Wombat; 13.05.2016
comment
Я получаю "0" и "0.0" от "#.#" и "0.0" от 0.06, поэтому я решил, что они одинаковы. Этот ответ очень полезен, кстати. Большое спасибо - person 4castle; 13.05.2016
comment
Я собираюсь принять этот ответ, поскольку он включает в себя, как получить ожидаемый результат, но большое спасибо обоим ответам за понимание. Я буду считать 0.06 ошибкой, потому что я не могу воспроизвести ее и в других версиях. - person 4castle; 13.05.2016

Когда вы делаете

df.format(0.15);

на самом деле две операции округления. Очевидным является тот, который вы просили с df.format, но есть более ранний, который происходит во время компиляции.

Десятичное значение 0,15 не может быть представлено как двойное. Двойники могут представлять только рациональные числа, знаменатель которых является степенью двойки, точно так же, как десятичная запись может представлять только рациональные числа, знаменатель которых является степенью числа десять. Когда вы пишете 0.15, компилятор округляет это значение до ближайшего значения, которое может быть представлено как двойное, и это значение оказывается равным

0.1499999999999999944488848768742172978818416595458984375

который df.format правильно округляется. Это не совсем середина между 0,1 и 0,2, поэтому режим округления HALF_UP не имеет значения.

Что касается

df.format(0.06); // expecting 0.1, getting 0.0  << unexpected

Если вы видите это, это ошибка. Похоже, он соответствует ссылке ScaryWombat, исправленной в 8u40 и 8у45. Тест на Ideone, который использует 8u51, показывает правильное поведение. Обновление Java должно решить проблему.

person user2357112 supports Monica    schedule 13.05.2016
comment
Мне это нравится: удвоения могут представлять только рациональные числа, знаменатель которых является степенью двойки. - person tofutim; 13.05.2016
comment
Можете ли вы тогда объяснить df.format(0.06); // expecting 0.1, getting 0.0 << unexpected - а также почему OK для 1,7, а не 1,8? - person Scary Wombat; 13.05.2016
comment
@user2357112 user2357112 Вы хотели использовать 0,0… вместо 0,1… после того, как сначала упомянули пример 0,05? Кроме того, превосходный ответ. - person Basil Bourque; 13.05.2016
comment
@BasilBourque: Упс. Я хотел использовать 0,15 вместо 0,05. - person user2357112 supports Monica; 13.05.2016

Я думаю, что ответом является Стандарт IEEE для арифметики с плавающей запятой (IEEE 754). В статье Wiki есть хороший пример в разделе правил округления. Также вы можете прочитать раздел 9.1 Введения в Java, в котором больше рассказывается о плавающей запятой. BigDecimal устраняет большинство этих недостатков, сохраняя значение внутри BigInteger, точность и масштаб в отдельных целочисленных полях.

person Kalenda    schedule 13.05.2016