Как использовать BigDecimal в Java

Предыдущая  Содержание  Следующая V*D*V

http://www.opentaps.org/docs/index.php/How_to_Use_Java_BigDecimal:_A_Tutorial

Суть проблемы

Когда мы начали создавать сервисы General Ledger для бухгалтерского учета, то обнаружили, что во многих местах были ошибки в 0.01 или более процентов. Это делает учёт денег практически невозможным. Кто хотел бы выставить счёт клиенту на $4.01, когда в его заказе $4.00?

 

Причины этих ошибок стали понятны достаточно быстро: расчёты, которые содержат суммы, количества, коррективы, и многие другие вещи обычно выполняются с малым или вообще без внимания к особенностям проблем точности и округления, которые возникают при работе с финансами.

 

Все эти вычисления использовали тип Java double, который не предлагает способа управления округлением числа или ограничения точности в расчётах. Мы пришли к решению, включающему использование java.math.BigDecimal, что дает нам управлять всем этим.

 

Эта статья - пример проблем математики финансов и учебник по использованию BigDecimal в целом.

Пример проблем при учёте финансов

Денежные расчёты требуют точности в заданной степени, например, для большинства валют это два знака после запятой. Они также требуют определённого типа поведения при округлении, например, в случае налогов всегда выполнять округление в большую сторону.

 

Например, предположим, что у нас есть продукт, который стоит 10.00 в заданной валюте и местный налог с продаж 0.0825, или 8.25%. Если посчитать налог на бумаге, сумма будет:

 

10.00 * 0.0825 = 0.825

 

Поскольку точность расчёта для данной валюты две цифры после запятой, требуется округлить число 0.825. Кроме того, поскольку это налог, обычной практикой является постоянное округление до цента в большую сторону. Таким образом, после расчёта баланса по счетам в конце дня мы никогда не получим недоплату налогов.

 

0.825 -> 0.83

 

И таким образом клиенту выставляется общий счёт на сумму 10.83 в местной валюте, а сборщику налогов выплачивается 0.83. Обратите внимание, что если продать 1000 таких продуктов, то переплата сборщику налогов была бы:

 

1000 * (0.83 - 0.825) = 5.00

 

Другой важный вопрос: где делать округление в данном расчёте. Предположим, жидкий азот продаётся по цене 0.528361 за литр. Клиент приходит и покупает 100.00 литров, поэтому посчитаем полную цену:

 

100.0 * 0.528361 = 52.8361

 

Так как это не налог, можно округлить эту цифру вверх или вниз на своё усмотрение. Предположим, округление выполняется в соответствии со стандартными правилами округления: если следующая значащая цифра меньше 5, округляем в меньшую сторону. В противном случае округляем вверх. Это даёт для окончательной цены значение 52.84.

 

Теперь предположим, что мы хотим дать рекламную скидку в размере 5% от всей покупки. Делать скидку с цифры 52.8361 или 52.84? Какова разница?

 

Расчёт 1: 52.8361 * 0.95 = 50.194295 = 50.19

Расчёт 2: 52.84 * 0.95 = 50.198 = 50.20

 

Обратите внимание, что окончательная цифра округлена по стандартному правилу округления.

 

Видите разницу в один цент между двумя цифрами? Старый код не беспокоился о принятии во внимание округления, поэтому он всегда делал вычисления как в Расчёте 1. Но в новом коде перед расчётом скидок, налогов и всего другого сначала выполняется округление, как и в Расчёте 2. Это одна из главных причин для ошибки в один цент.

Знакомство с BigDecimal

Из примеров в предыдущем разделе должно стать ясным, что необходимы две вещи:

 

1.Возможность задать масштаб, который представляет собой количество цифр после десятичной точки

2.Возможность задать метод округления

 

Класс java.math.BigDecimal принимает во внимание оба этих соображения. Смотрите документацию по BigDecimal.

 

Создать BigDecimal из (скалярного) числа типа double просто:

 

bd = new BigDecimal(1.0);

 

Чтобы создать BigDecimal из Double, сначала воспользуйтесь методом doubleValue().

 

Однако лучшей идеей является создание из строки:

 

bd = new BigDecimal("1.5");

 

Если этого не сделать, то результат будет следующим:

 

bd = new BigDecimal(1.5);

bd.toString(); // => 0.1499999999999999944488848768742172978818416595458984375

Округление и масштабирование

Чтобы задать количество цифр после запятой, используйте метод .setScale(scale). Тем не менее, хорошей практикой является одновременное указание вместе с масштабом режима округления с помощью .setScale(scale, roundingMode). Режим округления задаёт правило округления числа.

 

Почему желательно указать режим округления? Давайте в качестве примера используем приведённый выше BigDecimal из 1.5:

 

bd = new BigDecimal(1.5); // на самом деле 1.4999....

bd.setScale(1); // получаем исключение ArithmeticException

 

Будет сгенерировано исключение, потому что не известно, как округлить 1.49999. Так что всегда использовать .setScale(scale, roundingMode) - это хорошая идея.

 

Есть восемь вариантов режима округления:

 

ROUND_CEILING: В большую сторону

 

 0.333  ->   0.34 

-0.333  ->  -0.33

 

ROUND_DOWN: Отбрасывание разряда

 

 0.333  ->   0.33 

-0.333  ->  -0.33

 

ROUND_FLOOR: В меньшую сторону

 

 0.333  ->   0.33

-0.333  ->  -0.34

 

ROUND_HALF_UP: Округление вверх, если число после запятой >= .5

 

0.5  ->  1.0

0.4  ->  0.0

 

ROUND_HALF_DOWN: Округление вверх, если число после запятой > .5

 

0.5  ->  0.0

0.6  ->  1.0

 

ROUND_HALF_EVEN:

 

Округление половины по чётности округляет как обычно. Однако, когда округляемая цифра 5, округление будет идти вниз, если цифра слева от 5 чётная и вверх, если нечётная. Это лучше всего иллюстрируется примером:

 

a = new BigDecimal("2.5"); // цифра слева от 5 чётная, поэтому округление вниз

b = new BigDecimal("1.5"); // цифра слева от 5 нечётная, поэтому округление вверх

a.setScale(0, BigDecimal.ROUND_HALF_EVEN).toString() // => 2

b.setScale(0, BigDecimal.ROUND_HALF_EVEN).toString() // => 2

 

Документация Java говорит о ROUND_HALF_EVEN так: обратите внимание, что это такой режим округления, который сводит к минимуму совокупную ошибку когда при выполнении последовательности вычислений постоянно выполняется округление.

 

ROUND_UNNECESSARY:

 

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

 

При делении BigDecimals будьте осторожны и указывайте способ округления в методе .divide(...). В противном случае можно получить ArithmeticException, если нет точного округлённого результирующего значения, например, 1/3. Таким образом, всегда следует делать так:

 

a = b.divide(c, decimals, rounding);

Неизменяемость и арифметика

Числа BigDecimal являются неизменными. Это означает, что если создаётся новый объект BigDecimal со значением "2.00", такой объект останется "2.00" и никогда не может быть изменён.

 

Так как же тогда выполняются математические расчёты? Методы .add(), .multiply() и другие возвращают новый объект BigDecimal, содержащий результат. Например, чтобы получить результат при расчёте суммы:

 

amount = amount.add( thisAmount );

 

Убедитесь, что вы не делаете это так:

 

amount.add( thisAmount );

 

ЭТО САМАЯ РАСПРОСТРАНЁННАЯ ОШИБКА ПРИ РАБОТЕ С BIGDECIMAL!

Сравнение

Важно никогда не использовать для сравнения BigDecimal метод .equals(). Этого нельзя делать потому, что функция equals будет сравнивать масштабы. Если масштабы различаются, .equals() вернёт ложь, даже если они математически равны:

 

BigDecimal a = new BigDecimal("2.00");

BigDecimal b = new BigDecimal("2.0");

print(a.equals(b)); // ложь

 

Вместо этого следует использовать методы .compareTo() и .signum().

 

a.compareTo(b); // возвращает (-1 если a < b), (0 если a == b), (1 если a > b)

a.signum(); // возвращает (-1 если a < 0), (0 если a == 0), (1 если a > 0)

Где делать округление: размышления о точности

Теперь, когда есть возможность управлять округлением расчёта, до какого знака следует округлять? Ответ зависит от того, как планируется использовать полученное число.

 

Вам известна требуемая точность конечного результата из потребностей пользователей. Для чисел, которые будут складываться и вычитаться для получения конечного результата, необходимо добавить ещё один десятичный разряд, так что сумма 0.0144 + 0.0143 будет округлена до 0.03, в то время как если округление выполняется до 0.01, результатом будет 0.02.

 

Если необходимы числа, которые будут умножаться для получения конечного результата, необходимо сохранять столько знаков после запятой, сколько возможно. Например, коэффициенты и удельные затраты не должны округляться. После умножения необходимо округлять конечный результат.

 

Предыдущая  Содержание  Следующая