Почему для финансовых расчетов используют BigDecimal, а не float или double?
Что проверяют
Знаете ли вы, почему двоичная плавающая точка не хранит десятичные доли точно, и умеете ли пользоваться BigDecimal правильно. Про саму проблему слышали почти все, спотыкаются на конструкторе и на сравнении.
Как отвечать
double и float это двоичная дробь: мантисса умножается на степень двойки. Значения вроде 0.1 и 0.2 в такой форме непредставимы и округляются до ближайшего двоичного, поэтому их сумма даёт 0.30000000000000004. На одной операции расхождение незаметно, на миллионе начислений оно выходит в баланс.
BigDecimal хранит целое произвольной длины и десятичный порядок, то есть значение равно целому, делённому на степень десяти. Арифметика точная, правило округления задаётся явно.
BigDecimal total = new BigDecimal("0.1").add(new BigDecimal("0.2"));
BigDecimal vat = total.multiply(new BigDecimal("0.20"))
.setScale(2, RoundingMode.HALF_UP);Три правила, которые стоит назвать сразу. Создавать только из строки либо через BigDecimal.valueOf, потому что конструктор от double затаскивает внутрь ровно тот двоичный мусор, от которого уходили. Делить только с указанием масштаба и режима округления, иначе метод бросит исключение на непериодическом частном. Сравнивать через compareTo.
Альтернатива: хранить сумму целым в минорных единицах. Быстро и точно для сложения, но проценты всё равно требуют явного правила округления. В базе колонка десятичного типа с фиксированным масштабом, и тот же масштаб указывается в отображении сущности.
Где спотыкаются
Пишут конструктор от числа с точкой вместо строки. Результат сохраняет длинный двоичный хвост, и весь смысл перехода теряется, причём тихо: ошибка всплывёт через несколько операций в отчёте.
Сравнивают через equals. Этот метод учитывает и значение, и масштаб, поэтому единица с одним знаком после точки не равна единице с двумя. По той же причине такие объекты плохо ведут себя ключами в хеш-таблицах.
Забывают про неизменяемость. Методы возвращают новый объект, а вызов ради побочного эффекта не меняет ничего, и накопление суммы в цикле молча даёт исходное значение. Компилятор здесь не помогает, ловит это только тест на итоговую сумму.