Потеря точности преобразования из java BigDecimal в double

Я работаю с приложением, которое полностью основано на двойниках, и у меня возникают проблемы с одним утилитным методом, который анализирует строку в 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 max= 2 K-1 -1, а стандартная реализация имеет K = 11. Итак, максимальный показатель должен быть 511, no?

Ответ 1

Вы достигли максимальной точности для 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 your number that doesn't fit into a double
299792.458000000000 // 0010010011000100000111010100111111011111001110110110
299792.458000000040 // 0010010011000100000111010100111111011111001110110111

Ответ 2

Проблема заключается в том, что a 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());

Ответ 3

Печатается только то, что многие цифры печатаются так, что при повторной синтаксической обработке строки она будет равна одному и тому же значению.

Некоторые подробности можно найти в javadoc для Double # toString

Сколько цифр должно быть напечатано для дробной части m или a? Должна быть хотя бы одна цифра, чтобы представлять дробную часть, а за ее пределами - столько, сколько нужно, но больше, чем столько цифр, сколько необходимо для однозначного отличия значения аргумента от смежных значений типа double. То есть предположим, что x - точное математическое значение, представленное десятичным представлением, созданным этим методом, для конечного ненулевого аргумента d. Тогда d должно быть двойным значением, ближайшим к x; или если два двойных значения одинаково близки к x, то d должен быть одним из них, а младший значащий бит значения d должен быть 0.

Ответ 4

Если это полностью основано на удвоениях... почему вы используете BigDecimal? Разве Double не имеет смысла? Если это слишком большое значение (или слишком большая точность) для этого, то... вы не можете его преобразовать; это послужило бы основанием для использования BigDecimal.

Что касается того, почему он теряет точность, из javadoc

Преобразует этот BigDecimal в double. Это преобразование похоже на сужение примитивного преобразования из double в float, как определено в Спецификации языка Java: если этот BigDecimal имеет слишком большую величину, представляя его как double, он будет преобразован в Double.NEGATIVE_INFINITY или Double.POSITIVE_INFINITY, если это необходимо. Обратите внимание, что даже когда возвращаемое значение является конечным, это преобразование может потерять информацию о точности значения BigDecimal.

Ответ 5

Вы достигли максимально возможной точности для двойника. Если вы все равно хотите сохранить значение в примитивах... одним из возможных способов является сохранение части до десятичной точки в длинном

long l = 299792;
double d = 0.457999999984;

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