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

Форматированный double демонстрирует потерю точности после третьего места (приложение требует этих более низких мест точности).

Как я могу заставить BigDecimal сохранить эти дополнительные места точности?

Спасибо!


Обновите после просмотра этого сообщения. Некоторые люди упоминают, что это превышает точность типа данных double. Если я не читаю эту ссылку неправильно: http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.2.3 тогда двойной примитив имеет максимальное экспоненциальное значение E max = 2 K-1 -1, а в стандартной реализации K = 11. Итак, максимальный показатель должен быть 511, не так ли?


person Edward Q. Bridges    schedule 21.04.2011    source источник


Ответы (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
person WhiteFang34    schedule 21.04.2011
comment
Почти правильно; измените режим округления, и вы решили проблему OP - person Anon; 22.04.2011
comment
@Anon: О чем ты говоришь? Нет решения проблемы OP с помощью double. - person WhiteFang34; 22.04.2011
comment
См. Мой ответ или перечитайте вопрос ОП, чтобы понять его / ее проблему с потерей 9 - person Anon; 22.04.2011
comment
@Anon: Неважно, округляете ли вы вверх или вниз, точность все равно теряется. В ОП заявили, что точность должна быть сохранена. Больше девяток не означает большей точности, это просто означает, что вы округляете в меньшую сторону. - person WhiteFang34; 22.04.2011
comment
Процитируем OP: он потерял точность после третьего места Он / она не ищет 17 цифр или что-то еще, но сохраняет 15 цифр, которые может содержать двойная. Является ли это серьезным беспокойством или нет, это его / ее забота. - person Anon; 22.04.2011
comment
Возможно, ОП ошибочно подумал, что точность сохраняется только до 3 цифр после десятичной дроби. Если OP требует еще несколько цифр после них, тогда 0 на самом деле является ближайшим представлением. В таком случае делать нечего. Принудительное округление пола приведет к потере большей точности, чем необходимо. - person WhiteFang34; 22.04.2011
comment
Теперь, когда вы на это указываете, это довольно очевидно. Мне действительно нужно содержать все цифры точности, но похоже, что он поддерживает 11 разрядов - с округлением - чего может быть достаточно. В противном случае мне нужно будет выполнить рефакторинг до BigDecimal. Спасибо! - person Edward Q. Bridges; 22.04.2011
comment
Прочитав больше, добавил обновление выше. Пожалуйста, обратитесь и поясните, спасибо! - person Edward Q. Bridges; 27.04.2011

Проблема в том, что 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());
person Anon    schedule 21.04.2011

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

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

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

person Jörn Horstmann    schedule 21.04.2011

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

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

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

person Brian Roach    schedule 21.04.2011
comment
И это относится к вопросу ОП, как? Вы заглядывали в JLS, чтобы узнать о сужении примитивных преобразований? - person Anon; 22.04.2011

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

long l = 299792;
double d = 0.457999999984;

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

person Java Drinker    schedule 21.04.2011
comment
Но лучшим решением в целом было бы использовать BigDecimal повсюду, который предназначен для использования для чисел произвольной точности. - person Andrzej Doyle; 12.05.2011
comment
согласился, хотя почему-то у меня сложилось впечатление, что ОП искал альтернативу BigDecimal. - person Java Drinker; 13.05.2011
comment
Это теряет экспоненциальный диапазон удвоения для больших чисел и не приносит пользу маленьким числам. Если вы собираетесь пойти по этому пути, имеет смысл просто использовать фиксированную точку (например, с дробной частью в качестве числителя более 2 ^ 63 или 2 ^ 64). В качестве альтернативы, представьте число как сумму удвоений (т. Е. Приближение плюс некоторые ошибки); есть даже способы делать точные суммы с помощью этого (например, в Python [math.fsum ()]), но я не уверен, насколько легко это обобщается на умножение / деление. - person tc.; 10.06.2013