Java: Почему мы должны использовать BigDecimal вместо Double в реальном мире?

Когда речь идет о реальных денежных величинах в мире, мне рекомендуется использовать BigDecimal вместо Double.But, у меня нет убедительного объяснения, кроме как "Обычно это делается".

Можете ли вы пролить свет на этот вопрос?

Ответ 1

Это называется потерей точности и очень заметно при работе с очень большими числами или очень маленькими числами. Бинарное представление десятичных чисел с радиусом во многих случаях является приближением, а не абсолютным значением. Чтобы понять, почему вам нужно читать представление плавающего числа в двоичном формате. Вот ссылка: http://en.wikipedia.org/wiki/IEEE_754-2008. Вот быстрая демонстрация:
в bc (произвольный язык калькулятора точности) с точностью = 10:

(1/3 + 1/12 + 1/8 + 1/30) = 0,6083333332
(1/3 + 1/12 + 1/8) = 0,541666666666666
(1/3 + 1/12) = 0,416666666666666

Java double:
0.6083333333333333
0.5416666666666666
0.41666666666666663

Java float:

0.60833335
0.5416667
0.4166667


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

Ответ 2

Я думаю, что это описывает решение вашей проблемы: Ловушки Java: Большая Десятичная версия и проблема с двойным здесь

Из исходного блога, который теперь отключен.

Ловушки Java: double

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

  • Десятичные числа - это приближения

    Хотя все натуральные числа от 0 до 255 могут быть точно описаны с использованием 8 бит, описание всех действительных чисел между 0.0 - 255.0 требует бесконечного количества бит. Во-первых, существует бесконечное число чисел для описания в этом диапазоне (даже в диапазоне 0,0 - 0,1), а во-вторых, некоторые иррациональные числа не могут быть описаны численно вообще. Например, е и я. Другими словами, цифры 2 и 0.2 в компьютере по-разному представлены.

    Целые представляют собой биты, представляющие значения 2n, где n - позиция бит. Таким образом, значение 6 представляется как 23 * 0 + 22 * 1 + 21 * 1 + 20 * 0, соответствующее битовой последовательности 0110. Десятичные значения, с другой стороны, описываются битами, представляющими 2-n, то есть дробями 1/2, 1/4, 1/8,... Число 0,75 соответствует 2-1 * 1 + 2-2 * 1 + 2-3 * 0 + 2-4 * 0, что дает бит 1100 (1/2 + 1/4).

    Оборудовавшись этими знаниями, мы можем сформулировать следующее эмпирическое правило: любое десятичное число представлено приблизительным значением.

    Исследуем практические последствия этого, выполняя ряд тривиальных умножений.

    System.out.println( 0.2 + 0.2 + 0.2 + 0.2 + 0.2 );
    1.0
    

    1.0 печатается. Хотя это действительно правильно, это может дать нам ложное чувство безопасности. Кстати, 0.2 - одно из немногих значений, которые Java может правильно представлять. Пусть снова вызовет Java с другой тривиальной арифметической задачей, добавив число 0,1 десять раз.

    System.out.println( 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f );
    System.out.println( 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d );
    
    1.0000001
    0.9999999999999999
    

    Согласно слайдам из блога Джозефа Д. Дарси, суммы двух вычислений составляют 0.100000001490116119384765625 и 0.1000000000000000055511151231... соответственно. Эти результаты верны для ограниченного набора цифр. float имеют точность 8 ведущих цифр, в то время как double имеет 17 ведущих цифр. Теперь, если концептуальное несоответствие между ожидаемым результатом 1.0 и результатами, напечатанными на экранах, было недостаточным для того, чтобы вы могли набирать сигналы тревоги, а затем обратите внимание на то, как цифры от mr. Слайды Darcy, похоже, не соответствуют печатным номерам! Это еще одна ловушка. Подробнее об этом ниже.

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

    System.out.println( 0.3 == 0.1d + 0.1d + 0.1d );
    false
    

    Удивительно, что неточность уже срабатывает при трех дополнениях!

  • Переполнение двойников

    Как и любой другой простой тип в Java, double представляется конечным набором бит. Следовательно, добавление значения или умножение двойника может дать неожиданные результаты. Вполне, цифры должны быть довольно большими, чтобы переполнение, но это происходит. Попробуем умножить и разделить большое число. Математическая интуиция говорит, что результатом является исходное число. В Java мы можем получить другой результат.

    double big = 1.0e307 * 2000 / 2000;
    System.out.println( big == 1.0e307 );
    false
    

    Проблема заключается в том, что big сначала умножается, переполняется, а затем переполненное число делится. Хуже того, для программиста не возникает никаких исключений или других видов предупреждений. В принципе, это выражение x * y полностью ненадежно, поскольку в общем случае никаких указаний или гарантий не делается для всех двойных значений, представленных x, y.

  • Крупные и маленькие не друзья!

    Лавр и Харди часто не соглашались на многие вещи. Точно так же в вычислениях большие и малые не являются друзьями. Следствием использования фиксированного количества бит для представления чисел является то, что работа на действительно больших и очень малых числах в одних и тех же вычислениях не будет работать должным образом. Попробуем добавить что-то маленькое к чему-то большому.
    System.out.println( 1234.0d + 1.0e-13d == 1234.0d );
    true
    

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

  • Десятичные числа нельзя сравнивать напрямую

    То, что мы узнали до сих пор, состоит в том, что мы должны отбросить всю интуицию, которую мы получили в математическом классе и программирование с целыми числами. Используйте десятичные числа осторожно. Например, оператор for(double d = 0.1; d != 0.3; d += 0.1) является замаскированным бесконечным циклом! Ошибка заключается в сравнении десятичных чисел непосредственно друг с другом. Вы должны придерживаться следующих направляющих линий.

    Избегайте тестов равенства между двумя десятичными числами. Воздержитесь от if(a == b) {..}, используйте if(Math.abs(a-b) < tolerance) {..}, где допуска может быть константа, определенная, например, открытый статический конечный двойной допуск = 0,01 Рассмотрим в качестве альтернативы использование операторов <, > , поскольку они могут более естественно описывать то, что вы хотите выразить. Например, я предпочитаю форму for(double d = 0; d <= 10.0; d+= 0.1) за более неуклюжий for(double d = 0; Math.abs(10.0-d) < tolerance; d+= 0.1) Обе формы имеют свои достоинства в зависимости от ситуации: при модульном тестировании я предпочитаю выражать, что assertEquals(2.5, d, tolerance) по словам assertTrue(d > 2.5) не только первая форма читается лучше, часто это проверка, которую вы хотите делать (т.е. d не слишком велико).

  • WYSINWYG - то, что вы видите, не то, что вы получаете

    WYSIWYG - это выражение, обычно используемое в приложениях графического интерфейса пользователя. Это означает, что "То, что вы видите, что вы получаете", и используется в вычислениях для описания системы, в которой контент, отображаемый во время редактирования, очень похож на конечный результат, который может быть печатным документом, веб-страницей и т.д. фраза была изначально популярной фразой, созданной Flip Wilson drag persona "Geraldine", которая часто говорила "Что вы видите, это то, что вы получаете", чтобы оправдать ее странное поведение (из википедии).

    Другие серьезные программисты-ловушки часто падают, думают, что десятичные числа WYSIWYG. Необходимо понимать, что при печати или записи десятичного числа это не приближенное значение, которое печатается/записывается. По-разному, Java делает много приближений за кулисами и настойчиво пытается защитить вас от его вечного знания. Есть только одна проблема. Вы должны знать об этих приближениях, иначе вы можете столкнуться со всеми таинственными ошибками в своем коде.

    С некоторой изобретательностью, однако, мы можем исследовать, что действительно происходит за сценой. К настоящему времени мы знаем, что число 0,1 представлено некоторым приближением.

    System.out.println( 0.1d );
    0.1
    

    Мы знаем, что 0,1 не 0,1, а 0,1 - на экране. Вывод: Java - WYSINWYG!

    Для разнообразия позвольте выбрать еще один невинный номер, скажем 2.3. Подобно 0,1, 2,3 - приблизительное значение. Неудивительно, что при печати числа Java скрывает аппроксимацию.

    System.out.println( 2.3d );
    2.3
    

    Чтобы исследовать внутреннее приближенное значение 2.3, мы можем сравнить число с другими числами в близком диапазоне.

    double d1 = 2.2999999999999996d;
    double d2 = 2.2999999999999997d;
    System.out.println( d1 + " " + (2.3d == d1) );
    System.out.println( d2 + " " + (2.3d == d2) );
    2.2999999999999994 false
    2.3 true
    

    Итак, 2.2999999999999997 равно 2,3, как значение 2.3! Также обратите внимание, что из-за приближения точка поворота находится на уровне..99997, а не..99995, где вы обычно округляете по математике. Еще один способ получить приблизительное значение - вызвать службы BigDecimal.

    System.out.println( new BigDecimal(2.3d) );
    2.29999999999999982236431605997495353221893310546875
    

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

    Ничто не легко, и редко что-то приходит бесплатно. И "естественно", поплавки и удвоения дают разные результаты при печати/записи.

    System.out.println( Float.toString(0.1f) );
    System.out.println( Double.toString(0.1f) );
    System.out.println( Double.toString(0.1d) );
    0.1
    0.10000000149011612
    0.1
    

    Согласно слайдам из блога Джозефа Д. Дарси, поплавковая аппроксимация имеет 24 значащих бита, тогда как двойное приближение имеет 53 значащих бита. Мораль заключается в том, что для сохранения значений вы должны читать и записывать десятичные числа в том же формате.

  • Разделение на 0

    Многие разработчики знают по опыту, что деление числа на ноль приводит к резкому прекращению их приложений. Аналогичное поведение обнаружено в Java при работе с int, но, что удивительно, не при работе с double. Любое число, за исключением нуля, деленное на ноль, дает соответственно ∞ или -∞. Деление нуля на ноль приводит к специальному значению NaN, Not a Number.

    System.out.println(22.0 / 0.0);
    System.out.println(-13.0 / 0.0);
    System.out.println(0.0 / 0.0);
    Infinity
    -Infinity
    NaN
    

    Разделение положительного числа на отрицательное число дает отрицательный результат, а деление отрицательного числа на отрицательное число дает положительный результат. Так как деление на ноль возможно, вы получите другой результат в зависимости от того, разделите ли вы число с 0.0 или -0.0. Да, это правда! Java имеет отрицательный ноль! Не обманывайте себя, но два нулевых значения равны, как показано ниже.

    System.out.println(22.0 / 0.0);
    System.out.println(22.0 / -0.0);
    System.out.println(0.0 == -0.0);
    Infinity
    -Infinity
    true
    
  • Бесконечность странная

    В мире математики бесконечность была концепцией, которую мне трудно было понять. Например, я никогда не приобретал интуицию, когда одна бесконечность была бесконечно больше другой. Разумеется, Z > N множество всех рациональных чисел бесконечно больше множества натуральных чисел, но это было пределом моей интуиции в этом отношении!

    К счастью, бесконечность в Java примерно такая же непредсказуемая, как бесконечность в математическом мире. Вы можете выполнять обычные подозреваемые (+, -, *,/по бесконечному значению, но вы не можете применить бесконечность к бесконечности.

    double infinity = 1.0 / 0.0;
    System.out.println(infinity + 1);
    System.out.println(infinity / 1e300);
    System.out.println(infinity / infinity);
    System.out.println(infinity - infinity);
    Infinity
    Infinity
    NaN
    NaN
    

    Основная проблема здесь в том, что значение NaN возвращается без каких-либо предупреждений. Следовательно, если вы по-глупо расследуете, является ли конкретный двойник четным или нечетным, вы действительно можете попасть в волосатую ситуацию. Может быть, исключение во время выполнения было бы более подходящим?

    double d = 2.0, d2 = d - 2.0;
    System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1));
    d = d / d2;
    System.out.println("even: " + (d % 2 == 0) + " odd: " + (d % 2 == 1));
    even: true odd: false
    even: false odd: false
    

    Внезапно ваша переменная не является ни странной, ни даже! NaN даже страннее, чем бесконечность Бесконечное значение отличается от максимального значения двойника, а NaN снова отличается от бесконечного значения.

    double nan = 0.0 / 0.0, infinity = 1.0 / 0.0;
    System.out.println( Double.MAX_VALUE != infinity );
    System.out.println( Double.MAX_VALUE != nan );
    System.out.println( infinity         != nan );
    true
    true
    true
    

    В общем случае, когда двойник приобрел значение NaN, любая операция на нем приводит к NaN.

    System.out.println( nan + 1.0 );
    NaN
    
  • <сильные > Выводы

    • Десятичные числа - это приближения, а не значение, которое вы назначили. Любая интуиция, полученная в математическом мире, больше не применяется. Ожидайте a+b = a и a != a/3 + a/3 + a/3
    • Избегайте использования ==, сравнивайте с некоторым допуском или используйте команды >= или < =
    • Java - это WYSINWYG! Никогда не верьте, что значение, которое вы печатаете/записываете, является приблизительным значением, поэтому всегда читайте/записывайте десятичные числа в том же формате.
    • Соблюдайте осторожность, чтобы не переполнять свой двойник, чтобы не превратить ваш двойник в состояние ± бесконечность или NaN. В любом случае ваши расчеты могут оказаться не такими, как вы ожидали. Возможно, вам будет полезно всегда проверять эти значения перед возвратом значения в ваши методы.

Ответ 3

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

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

Ответ 4

Это прежде всего сделано по соображениям точности. BigDecimal хранит числа с плавающей запятой с неограниченной точностью. Вы можете взглянуть на эту страницу, которая объясняет это хорошо. http://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal

Ответ 5

Когда используется BigDecimal, он может хранить намного больше данных, чем Double, что делает его более точным и просто лучшим выбором для реального мира.

Хотя это намного медленнее и дольше, стоит того.

Ставка на то, что вы не хотели бы давать вашему боссу неточную информацию, а?

Ответ 6

Другая идея: отслеживать количество центов в long. Это проще и позволяет избежать громоздкого синтаксиса и низкой производительности BigDecimal.

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