потеря точности преобразования из java BigDecimal в double
Я работаю с приложением, которое полностью основано на двойниках, и у меня возникли проблемы в одном методе утилиты, который анализирует строку в двойник. Я нашел исправление, где использование BigDecimal для преобразования решает проблему, но вызывает другую проблему, когда я иду, чтобы преобразовать BigDecimal обратно в double: я теряю несколько мест точности. Например:
import java.math.BigDecimal;
import java.text.DecimalFormat;
public class test {
public static void main(String [] args){
String num = "299792.457999999984";
BigDecimal val = new BigDecimal(num);
System.out.println("big decimal: " + val.toString());
DecimalFormat nf = new DecimalFormat("#.0000000000");
System.out.println("double: "+val.doubleValue());
System.out.println("double formatted: "+nf.format(val.doubleValue()));
}
}
это производит следующий вывод:
$ java test
big decimal: 299792.457999999984
double: 299792.458
double formatted: 299792.4580000000
форматированный двойной демонстрирует что он потерял точность после третьего места (приложение требует этих более низких мест точности).
Как я могу получить BigDecimal, чтобы сохранить эти дополнительные места точности?
спасибо!
обновление после наверстывания этого поста. Несколько человек упоминают, что это превышает точность двойного типа данных. Если я не читаю эту ссылку неправильно: http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.2.3 тогда двойной примитив имеет максимальное экспоненциальное значение EМакс = 2к-1-1, а стандартная реализация имеет K=11. Так, максимальный показатель должен быть 511, нет?
5 ответов
вы достигли максимальной точности для double
С этим номером. Это невозможно. В этом случае значение округляется. Преобразование из BigDecimal
не связано, и проблема точности одинакова в любом случае. См. это, например:
System.out.println(Double.parseDouble("299792.4579999984"));
System.out.println(Double.parseDouble("299792.45799999984"));
System.out.println(Double.parseDouble("299792.457999999984"));
вывод:
299792.4579999984
299792.45799999987
299792.458
для этих случаев double
более 3 цифр после запятой. Они просто оказываются нулями для вашего числа, и это самое близкое представление, которое вы можете поместиться в double
. В этом случае он ближе к округлению, поэтому ваши 9, похоже, исчезают. Если вы попробуете это:
System.out.println(Double.parseDouble("299792.457999999924"));
вы заметите, что он держит ваши 9, потому что он был ближе к округлить вниз:
299792.4579999999
Если вам это нужно все цифр в вашем номере будут сохранены, то вам придется изменить код, который работает на double
. Вы могли бы использовать BigDecimal
вместо них. Если вам нужна производительность, вы можете изучить BCD как вариант, хотя я не знаю о каких-либо библиотеках навскидку.
в ответ на ваше обновление: максимальный показатель для числа с плавающей запятой двойной точности фактически равен 1023. Но это не твой ограничивающий фактор. Ваш номер превышает точность 52 дробные биты, которые представляют собой мантиссу, см. IEEE 754-1985.
использовать это преобразование с плавающей запятой чтобы увидеть ваш номер в двоичный. Показатель равен 18, так как 262144 (2^18) является ближайшим. Если вы возьмете дробные биты и пойдете вверх или вниз по одному в двоичном формате, вы увидите, что для представления вашего числа недостаточно точности:
299792.457999999900 // 0010010011000100000111010100111111011111001110110101
299792.457999999984 // here's your number that doesn't fit into a double
299792.458000000000 // 0010010011000100000111010100111111011111001110110110
299792.458000000040 // 0010010011000100000111010100111111011111001110110111
проблема в том, что double
может содержать 15 цифр, в то время как BigDecimal
может содержать произвольное число. Когда вы звоните toDouble()
пытается применить режим округления, чтобы удалить лишние цифры. Однако, поскольку у вас много 9 в выходе, это означает, что они продолжают округляться до 0, с переносом на следующую самую высокую цифру.
чтобы сохранить как можно больше точности, вам нужно изменить режим округления BigDecimal, чтобы он усекал:
BigDecimal bd1 = new BigDecimal("12345.1234599999998");
System.out.println(bd1.doubleValue());
BigDecimal bd2 = new BigDecimal("12345.1234599999998", new MathContext(15, RoundingMode.FLOOR));
System.out.println(bd2.doubleValue());
только то, что много цифр печатаются так, что при разборе строки обратно в double, это приведет к точно такому же значению.
некоторые детали можно найти в Javadoc для Double#toString
сколько цифр должно быть напечатано для дробной части m или a? Для представления дробной части должна быть по крайней мере одна цифра, а кроме того, столько, но только столько, сколько необходимо для уникального различения аргумента значение из смежных значений типа double. То есть предположим, что x-точное математическое значение, представленное десятичным представлением, полученным этим методом для конечного ненулевого аргумента d. Тогда d должно быть двойным значением, ближайшим к x; или если два двойных значения одинаково близки к x, то d должно быть одним из них, а наименьший значимый бит сигнификанда d должен быть 0.
если он полностью основан на парном разряде ... почему вы используете BigDecimal
? Не Double
больше смысла? Если это слишком большая ценность (или слишком большая точность) для этого ... вы не можете преобразовать его; это было бы причиной использовать BigDecimal в первую очередь.
Что касается того, почему он теряет точность, из javadoc
преобразует этот BigDecimal в double. Это преобразование аналогично сужающемуся примитивному преобразованию из double в float, как определено в спецификации языка Java: если этот BigDecimal имеет слишком большую величину, представленную как double, он будет преобразован в Double.NEGATIVE_INFINITY или Double.POSITIVE_INFINITY по мере необходимости. Обратите внимание, что даже если возвращаемое значение конечное, это преобразование может потерять информацию о точности BigDecimal значения.
вы попали максимально возможную точность для двойника. Если вы все еще хотите сохранить значение в примитивах... один из возможных способов-сохранить часть перед десятичной запятой в длинном
long l = 299792;
double d = 0.457999999984;
поскольку вы не используете (это плохой выбор слов) точность для хранения десятичного раздела, вы можете удерживать больше цифр точности для дробного компонента. Это должно быть достаточно легко сделать с некоторым округлением и т. д..