потеря точности преобразования из Java BigDecimal в удвоение - PullRequest
11 голосов
/ 22 апреля 2011

Я работаю с приложением, которое полностью основано на double, и у меня возникли проблемы с одним утилитарным методом, который анализирует строку в double.Я нашел исправление, в котором использование BigDecimal для преобразования решает проблему, но поднимает еще одну проблему, когда я собираюсь преобразовать BigDecimal обратно в двойное число: я теряю несколько мест точности.Например:

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, нет?

Ответы [ 5 ]

21 голосов
/ 22 апреля 2011

Вы достигли максимальной точности для 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
3 голосов
/ 22 апреля 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());
2 голосов
/ 22 апреля 2011

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

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

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

0 голосов
/ 22 апреля 2011

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

long l = 299792;
double d = 0.457999999984;

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

0 голосов
/ 22 апреля 2011

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

Относительно того, почему он теряет точность, из Javadoc

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

...