В финансовых расчетах часто приходится оперировать дробными числами. Например, на некоторых рынках цены на опционы и фьючерсы могут иметь два и даже три знака после запятой. Цены на некоторые акции могут выражаться в пенни, так называемые penny-stocks. Для валидации ордеров перед отправкой на рынок надо проверять, не выходит ли указанная цена ордера за определенные пределы (price limits), рассчитанные от текущей рыночной цены инструмента. Например, надо вычислить +/-5% от цены 1.005. Казалось бы для выражения цены в Java идеально подходит примитивный тип double. Но это крупная, опасная ошибка!
Классическая книга «Effective Java» (вышло уже 3-е издание) совершенно справедливо не рекомендует использовать double для денег. См. «Item 60: Avoid float and double if exact answers are required«. Еще раньше Джошуа Блох писал об этом в другой своей классике «Java Puzzlers» (см. Puzzle 2).
Типы float и double не годятся для денежных расчетов. Вещественные числа представлены в компьютерах в виде двоичных чисел. Но большинство десятичных чисел (например, число 0,1 или любую иную отрицательную степень числа 10) невозможно точно представить в двоичной форме. Поэтому в компьютере ваши двоичные числа хранятся в приблизительном значении. Разумеется, и операции с такими «приблизительными» числами приводят «приблизительным» значениям. Умножьте в Java 96.835 на 10000 и удивитесь результату:
jshell> double a = 96.835 * 10000.0 a ==> 968349.9999999999
или классика: от 2 рублей отнять 1 руб. 10 копеек, сколько получится сдачи:
jshell> double a = 2.00 - 1.10 a ==> 0.8999999999999999
Представляете, как такой код будет работать в кассовом аппарате или в разменном автомате?
BigDecimal спешит на выручку
Для точной арифметики в Java есть отличный класс BigDecimal. Он позволяет задавать точность результата управлять округлением и работает отлично при умножениях и делениях дробных чисел. Но у него есть существенные недостатки!
Во-первых, этот класс неизменяемый (immutable), а значит каждая операция над объектом BigDecimal порождает новый объект BigDecimal с результатом операции. Точно также, например, операции над String порождают новые объекты String. Если вам требуется провести сложное вычисление с множеством делений и умножений, вы создадите много объектов класса BigDecimal, которые будут аллоцированы в heap, на что требуется время. JVM надо будет отслеживать все эти объекты, и увеличит нагрузку на сборщик мусора. В конце концов вы достигните лимита и произойдет полный сбор мусора с остановкой JVM.
Второй недостаток, операции в BigDecimal выполняются медленно, а код сложных операций в формулах выглядит громоздко и неудобно.
При работе с BigDecimal следует помнить о некоторых очень важных нюансах: Four common pitfalls of the BigDecimal class and how to avoid them. Если о них не знать или забыть, это может привести к неприятным ошибкам округления или вообще неверному поведению приложения.
Как видите, недостатков больше, чем достоинств. Если ваше приложение не так чувствительно к latency, то BigDecimal — отличный вариант. В моей практике было написание приложения для HR для вычисления зарплат и пенсий сотрудников банка, и там требование к latency было не критичным, а точность вычисления требовалась высокая, без всяких ошибок округления. BigDecimal использовался там без вопросов.
Примитивы int и long помогут там, где не поможет BigDecimal
Но ведь с ценами все же надо как-то работать? Что же использовать, если не BigDecimal? Как вариант можно воспользоваться арифметикой с фиксированой точкой. Здесь цены выражаются с помощью примитивов int и long, в которых мы условливаемся считать последние два или три знака — знаками после запятой. То есть пишем 96835, а считаем что это 96.835, т.е. любое последние три знака для данного конкретного случая — знаки после запятой. Тогда все деления будут целочисленными без дробных частей, все умножения будут тоже целочисленными без всяких странных бесконечных девяток. Просто при отображении цены для людей или для конвертации в текстовое FIX-сообщение мы переводим число в строку и ставим десятичную точку перед третьим знаком с конца
На некоторых биржах (так, например, было в реализации FIX на Сиднейской фьючерсной бирже до 20 марта 2017 года) для этого используется так называемый price multiplier. То есть вы в FIX сообщении с биржи получаете число в поле «цена» 56789 для какого-то инструмента, а price multiplier в спецификации этого инструмента, например, «100» говорит вам, где в этой цене стоит десятичная точка. Значит цена равна «567.89». И то же самое преобразование надо сделать, когда вы отправляете на биржу сообщения. Здесь, кстати, еще одна польза — экономится 1 байт при передаче из-за того, что выбрасывается точка из цены.
Главный недостаток этого подхода — это трудности с написанием кода. Надо постоянно держать в голове это условие про позицию фиксированной точки в числе, не забывать о ней и не перепутать с другими переменными, которые выражают например количество (qty).
Выводы
Единого решения для всех случаев нет. Для каких-то случаев подойдет BigDecimal, для каких-то — int или long, а для каких-то конкретных — double с учетом всех нюансов. Например, если цена «никогда» не бывает дробной. Это бывает при торговле акциями, но не деривативами, где на некоторые экзотические инструменты цена бывает равна 0.0 или даже отрицательной!