Достаточно ли преобразовать double в BigDecimal непосредственно перед сложением, чтобы сохранить исходную точность?

Вопрос задан: 7 месяцев назад Последняя активность: 7 месяцев назад
up 0 down

Мы решаем ошибку, связанную с числовой точностью. Наша система собирает некоторые числа и выплевывает их сумму. Проблема в том, что система не сохраняет числовую точность, например, 300,7 + 400,9 = 701,599... в то время как ожидаемый результат будет 701,6. Предполагается, что точность адаптируется к входным значениям, поэтому мы не можем просто округлять результаты до фиксированной точности.

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

Путь данных следующий:

  1. XML-файл, тип xsd: десятичный
  2. Разобрать в примитив java двойник. 15 десятичных знаков должно быть достаточно, мы ожидаем значения не более 10 цифр, 5 цифр дроби.
  3. Хранить в БД MySql 5.5, тип double
  4. Загрузка через Hibernate в сущность JPA, т.е. по-прежнему примитивный двойной
  5. Сумма куча этих значений
  6. Распечатать сумму в другой файл XML

Теперь я предполагаю, что оптимальным решением было бы преобразование всего в десятичный формат. Неудивительно, что существует давление, чтобы пойти с самым дешевым решением. Оказывается, преобразование удваивается BigDecimal перед добавлением пары чисел работает в случае B в следующем примере

import java.math.BigDecimal;

public class Arithmetic {
    public static void main(String[] args) {
        double a = 0.3;
        double b = -0.2;
        // A
        System.out.println(a + b);//0.09999999999999998
        // B
        System.out.println(BigDecimal.valueOf(a).add(BigDecimal.valueOf(b)));//0.1
        // C
        System.out.println(new BigDecimal(a).add(new BigDecimal(b)));//0.099999999999999977795539507496869191527366638183593750
    }
}

Подробнее об этом: Почему нам нужно преобразовать double в строку, прежде чем мы сможем преобразовать его в BigDecimal? Непредсказуемость конструктора BigDecimal (double)

Я волнуюсь, что такой обходной путь будет бомбой замедленного действия. Во-первых, я не уверен, что эта арифметика является пуленепробиваемой для всех случаев. Во-вторых, все еще существует некоторый риск того, что кто-то в будущем может осуществить некоторые изменения и изменить B на C, потому что эта ловушка далеко не очевидна, и даже модульный тест может не выявить ошибку.

Я был бы готов согласиться со вторым пунктом, но вопрос заключается в следующем: будет ли этот обходной путь давать правильные результаты? Может ли быть случай, когда как-то

Double.valueOf("12345.12345").toString().equals("12345.12345")

это ложь? Учитывая, что Double.toString, согласно javadoc, печатает только те цифры, которые необходимы для уникального представления лежащего в основе двойного значения, так что при повторном анализе это дает то же самое двойное значение? Разве этого недостаточно для этого случая использования, когда мне нужно только добавить цифры и распечатать сумму с этим волшебным Double.toString (Double d) метод? Чтобы было ясно, я предпочитаю то, что считаю чистым решением, везде использующим BigDecimal, но мне не хватает аргументов для его продажи, в идеале я имею в виду пример, когда преобразование в BigDecimal до добавления не выполняет работу, описанную выше. ,

2 ответа

up 2 down accepted

Если вы не можете избежать разбора в примитив double или хранить как двойной, вы должны преобразовать в BigDecimal как можно раньше.

double не может точно представлять десятичные дроби. Значение в double x = 7.3; никогда не будет точно 7,3, но что-то очень очень близко к нему, с разницей, видимой от 16-й цифры или около того справа (давая 50 десятичных знаков или около того). Не вводите в заблуждение тот факт, что печать может дать ровно «7,3», поскольку печать уже выполняет какое-то округление и не показывает точное число.

Если вы делаете много вычислений с double числа, крошечные различия в конечном итоге будут суммироваться, пока они не превысят вашу терпимость. Таким образом, использование двойных чисел в вычислениях, где требуются десятичные дроби, действительно бомба замедленного действия.

[...] мы ожидаем значения не более 10 цифр, 5 цифр дроби.

Я прочитал это утверждение, чтобы означать, что все числа, с которыми вы имеете дело, должны быть точными кратными 0.00001, без каких-либо дополнительных цифр. Вы можете конвертировать двойные в такие BigDecimals с

new BigDecimal.valueOf(Math.round(doubleVal * 100000), 5)

Это даст вам точное представление числа с 5 десятичными дробными цифрами, 5-значное число, которое ближе всего к входу doubleVal. Таким образом, вы исправляете крошечные различия между doubleVal и десятичное число, которое вы изначально имели в виду.

Если бы вы просто использовали BigDecimal.valueOf(double val), вы бы прошли строковое представление используемого вами двойника, что не может гарантировать, что это то, что вы хотите. Это зависит от процесса округления внутри класса Double, который пытается представить двойное приближение 7,3 (возможно, 7,30000000000000123456789123456789125) с наиболее вероятным числом десятичных цифр. В результате получается «7.3» (и, слава разработчикам, довольно часто совпадает с «ожидаемой» строкой), а не «7.300000000000001» или «7.3000000000000012», которые оба кажутся мне одинаково правдоподобными.

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

Затем выполните вычисления с помощью BigDecimals (используя соответствующий MathContext для округления, если необходимо).

Когда вам, наконец, нужно сохранить число как двойное, используйте BigDecimal.doubleValue(). Результирующее двойное число будет достаточно близко к десятичному знаку, так что вышеупомянутое преобразование наверняка даст вам тот же BigDecimal, что у вас был раньше (если только у вас нет действительно огромных чисел, таких как 10 цифр перед десятичной запятой - вы потерялись с double тем не мение).

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

up 0 down

Это зависит от базы данных, которую вы используете. Если вы используете SQL Server, вы можете использовать тип данных как числовой (12, 8), где 12 представляют числовое значение, а 8 представляет точность. аналогично, для моего SQL DECIMAL (5,2) вы можете использовать.

Вы не потеряете значение точности, если будете использовать вышеупомянутый тип данных.

Java Hibernate Класс:

Вы можете определить частная двойная широта;

База данных:

Достаточно ли преобразовать double в BigDecimal непосредственно перед сложением, чтобы сохранить исходную точность?