Ошибка округления 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)
Ответы
Ответ 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