потеря точности преобразования из 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;

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