Главная проблема работа с деньгами в программировании заключается в том, что привычные нам десятичные дроби компьютер хранит в двоичной системе. Из-за этого 0.1 + 0.2 в double превращается в 0.30000000000000004. В финансовой сфере такая погрешность недопустима.
В Java есть три основных способа обойти эту проблему. Разберем их:
- Два
long: рубли и копейки отдельно
Первое, что приходит в голову - разделить сумму на две части и хранить рубли и копейки в разных переменных типа long.
long rubles = 100;
long kopecks = 50;
Плюсы: Максимальная точность, никаких проблем с двоичным представлением.
Минусы: Любая математическая операция превращается в сложный код. Вам нужно вручную переносить разряды при сложении копеек, учитывать знаки при отрицательных суммах и т.д. В общем ад для разработчика.
- Один
long: всё в копейках
Более прагматичный подход. Мы отказываемся от дробей и храним всю сумму в минорных единицах (в копейках, центах и т.д.).
long totalKopecks = 10050; // 100 рублей 50 копеек
Плюсы: Скорость работы примитивов, нет проблем с округлением при сложении и вычитании.
Минусы: Проблемы начинаются при умножении и делении. Например, при расчете налога 13% от 100.50 рублей вы получите 1306.5 копеек. Придется вручную реализовывать логику округления. Помимо этого становиться сложно работать с валютами, где нет минорных единиц.
BigDecimal: золото
Основная часть профессиональных финансовых систем на Java использует java.math.BigDecimal. Этот класс представляет число в виде массива цифр произвольной длины и отдельной переменной для позиции запятой. Он лишен проблем двоичного округления.
Имейте ввиду - создавать BigDecimal из double это ошибка:
// ТАК ДЕЛАТЬ НЕЛЬЗЯ
BigDecimal bad = new BigDecimal(0.1); // 0.1000000000000000055511151231257827021181583404541015625
// ТАК ПРАВИЛЬНО
BigDecimal good = new BigDecimal("0.1"); // Из строки
BigDecimal alsoGood = BigDecimal.valueOf(0.1); // Или через фабричный метод
Главная сила BigDecimal - строгий контроль над округлением. При делении всегда нужно указывать правило округления, иначе программа упадет с ArithmeticException, если результат это бесконечная дробь.
BigDecimal total = new BigDecimal("100.00");
BigDecimal percent = new BigDecimal("0.13"); // 13%
// Делим нацело с округлением до копеек
// RoundingMode.HALF_EVEN — банковское округление
BigDecimal tax = total.multiply(percent)
.setScale(2, RoundingMode.HALF_EVEN);
Итог
BigDecimal - выбор для 99% бизнес-приложений, банковских систем. Он медленнее примитивов, но спасает от критических багов с округлением и потерей точности.
- Хранение в минорных единицах - может подходить для того самого одно процента высоконагруженных систем, где важна каждая наносекунда.
- Два
long - cложность поддержки перевешивает любые плюсы.