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

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

Не могли бы вы пролить свет на этот вопрос?

6 ответов

Решение

Это называется потерей точности и очень заметно при работе с очень большими или очень маленькими числами. Двоичное представление десятичных чисел с основанием во многих случаях является приближенным, а не абсолютным значением. Чтобы понять, почему вам нужно прочитать о представлении с плавающей точкой в ​​двоичном виде. Вот ссылка: 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 двойной:
0,6083333333333333
0,5416666666666666
+0,41666666666666663

Java float:

0.60833335
0.5416667
0.4166667


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

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

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

Java-ловушки: двойные

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

  1. Десятичные числа являются приблизительными

    В то время как все натуральные числа в диапазоне от 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. Слайды Дарси не соответствуют напечатанным номерам! Это еще одна ловушка. Подробнее об этом ниже.

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

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

    Шокирующе, что неточность уже проявляется в трех дополнениях!

  2. Двойное переполнение

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

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

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

  3. Большие и маленькие не друзья!

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

    System.out.println( 1234.0d + 1.0e-13d == 1234.0d );
    true
    

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

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

    До сих пор мы узнали, что мы должны отбросить всю интуицию, которую мы приобрели в математическом классе и программировании с целыми числами. Используйте десятичные числа осторожно. Например, заявление 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 не слишком велико).

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

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

    Еще одна серьезная проблема, с которой часто сталкиваются программисты-ловушки, - думать, что десятичные числа - это 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 приблизительное значение. Неудивительно, что при печати числа Ява скрывает приближение.

    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 значащих бита. Мораль такова, что для сохранения значений вы должны читать и записывать десятичные числа в одном и том же формате.

  6. Деление на 0

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

    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
    
  7. Бесконечность странная

    В мире математики бесконечность была концепцией, которую мне было трудно понять. Например, я никогда не приобретал интуицию, когда одна бесконечность была бесконечно больше другой. Конечно, 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 еще более странный, чем бесконечность. Бесконечное значение отличается от максимального значения типа double, а 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
    
  8. Выводы

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам