Как считатать денежные значения
Почему нельзя использовать double для финансовых расчетов
В финансовых расчетах часто приходится оперировать дробными числами. Например, на некоторых рынках цены на опционы и фьючерсы могут иметь два или даже три десятичных знака. Цены на некоторые акции могут быть в пенсах, так называемые копеечные акции. Казалось бы, примитивный тип double идеально подходит для выражения цен. Но это большая и опасная ошибка!
Классическая книга «Эффективная Java» (на данный момент 3-е издание) не рекомендует использовать double для денежных вычислений. См. «Пункт 60: Избегайте чисел с плавающей запятой и двойной точности, если требуются точные ответы». Джошуа Блох писал об этом ранее в другой своей классической книге «Java Puzzlers» (см. «Головоломка 2»).
Типы float и double не подходят для известных всем денежных расчетов. Действительные числа представлены в компьютерах в виде двоичных чисел. Но большинство десятичных чисел (таких как число 0,1 или любое другое отрицательное число 10) невозможно представить в двоичной форме. Таким образом, ваши двоичные числа хранятся в приблизительном значении. Конечно, операции с такими числами приводят к не точным исчислениям. Умножьте в Java 96,835 на 10000 и удивитесь результату:
jshell> double a = 96.835 * 10000.0
a ==> 968349.9999999999
Если мы посчитаем цену в таком стиле, тот, кто будет работать с этой ценой дальше, будет удивлен.
Хорошая ли идея использовать BigDecimal везде?
В Java есть отличный класс BigDecimal. Он позволяет задавать точность результата, контролировать округление и отлично подходит для умножения и деления дробных чисел. Но у него есть существенные недостатки!
Во-первых, класс BigDecimal является неизменяемым, и каждая операция над объектом BigDecimal будет создавать новое выделение памяти. Что-то вроде объекта String. Если вам нужно выполнить сложное вычисление со множеством делений и умножений, вы создадите множество объектов BigDecimal, которые будут помещены в кучу, а это займет время. JVM необходимо будет отслеживать все эти объекты и увеличит нагрузку на сборщик мусора. В конце концов вы достигнете предела, произойдет полная сборка мусора и JVM остановится.
Во-вторых, операции в BigDecimal выполняются медленно, а код сложных операций в формулах выглядит громоздким и неудобным.
В-третьих, есть несколько важных моментов, которые следует помнить при использовании BigDecimal: [Четыре распространенные ошибки класса BigDecimal и способы их избежать](https://blogs.oracle.com/javamagazine/post/four-common-pitfalls- класса bigdecimal и как их избежать). Если они неизвестны или забыты, это может привести к неприятным ошибкам округления или даже некорректному поведению приложения.
Для многих приложений, чувствительных к задержке, эти недостатки более существенны, чем преимущества. Если ваше приложение не столь критично к задержкам, а точность вычислений должна быть без ошибок округления, BigDecimal — отличный выбор.
Использование long в расчетах
Что мы можем использовать для вычислений, если Bigdecimal не подходит для наших целей? В качестве альтернативы мы можем использовать арифметику с фиксированной точкой. В данном случае цены представляют собой примитивные типы int и long, в которых мы договорились считать последние две или три цифры десятичными частями. Например, пишем 10389, но считаем, что это цена 1,0389, т.е. любые последние три цифры для этого случая являются десятичной частью. После расчета нам необходимо преобразовать цену в удобочитаемый вид, чтобы поместить результат в FIX-сообщение или сериализовать в целевой протокол.
Benchmark
Бенчмарк позволяет почувствовать разницу между работой с различными представлениями чисел. Как мы видим, double имеет гораздо большую пропускную способность, чем BigDecimal.
Benchmark Mode Cnt Score Error Units
Arithmetic.aDouble thrpt 5 330631192.642 ± 9359440.323 ops/s
Arithmetic.aDouble:·gc.alloc.rate thrpt 5 ≈ 10⁻⁴ MB/sec
Arithmetic.aDouble:·gc.alloc.rate.norm thrpt 5 ≈ 10⁻⁷ B/op
Arithmetic.aDouble:·gc.count thrpt 5 ≈ 0 counts
Arithmetic.aLong thrpt 5 40676471.542 ± 3304307.569 ops/s
Arithmetic.aLong:·gc.alloc.rate thrpt 5 ≈ 10⁻⁴ MB/sec
Arithmetic.aLong:·gc.alloc.rate.norm thrpt 5 ≈ 10⁻⁶ B/op
Arithmetic.aLong:·gc.count thrpt 5 ≈ 0 counts
Arithmetic.baseline thrpt 5 4532377823.385 ± 354747798.199 ops/s
Arithmetic.baseline:·gc.alloc.rate thrpt 5 ≈ 10⁻⁴ MB/sec
Arithmetic.baseline:·gc.alloc.rate.norm thrpt 5 ≈ 10⁻⁸ B/op
Arithmetic.baseline:·gc.count thrpt 5 ≈ 0 counts
Arithmetic.bigDecimal thrpt 5 17153026.140 ± 1587551.803 ops/s
Arithmetic.bigDecimal:·gc.alloc.rate thrpt 5 2355.572 ± 218.022 MB/sec
Arithmetic.bigDecimal:·gc.alloc.rate.norm thrpt 5 144.000 ± 0.001 B/op
Arithmetic.bigDecimal:·gc.count thrpt 5 629.000 counts
Arithmetic.bigDecimal:·gc.time thrpt 5 424.000 ms
Заключение
Единого решения для всех случаев не существует. Для одних случаев подойдет BigDecimal, для других int или long, а для некоторых специфических случаев подойдет double, причем с учетом всех нюансов. Например, если цена "никогда" не бывает дробной.