Float и double вредны для финансового (даже военного) мира, никогда не используйте их для денежных расчетов. Если точность является одним из ваших требований, используйте
BigDecimal
вместо этого.
Давайте рассмотрим эту проблему на примере:
Все значения с плавающей запятой, которые могут представлять сумму в валюте (в долларах и центах), не могут храниться точно так же, как в памяти. Таким образом, если мы хотим хранить 0,1 доллара (10 центов), float / double не может хранить его как есть. Вместо этого двоичный файл может хранить только значение более близкого приближения (0,100000001490116119384765625 в десятичном виде). Масштабы этой проблемы становятся значительными (известными как потеря значимости), когда мы периодически выполняем арифметические операции (умножение или деление), используя эти два типа данных. Ниже мы покажем, как это может выглядеть.
Вот пример потери точности при использовании double:
public class DoubleForCurrency {
public static void main(String[] args) {
double total = 0.2;
for (int i = 0; i < 100; i++) {
total += 0.2;
}
System.out.println("total = " + total);
}
}
Выход программы:
total = 20.19999999999996
Выход должен был быть 20.20
(20 долларов и 20 центов), но вычисление с плавающей запятой сделало это 20.19999999999996
. Это потеря точности (или потеря значимости).
Причина потери значимости
Арифметика с плавающей точкой
В вычислениях арифметика с плавающей точкой (FP) является арифметикой, использующей формальное представление действительных чисел в качестве приближения для поддержки компромисса между дальностью и точностью.
Согласно Википедии :
«Будет ли рациональное число иметь конечное расширение, зависит от базы. Например, в базе-10 число 1/2 имеет конечное расширение (0,5), а число 1/3 — нет (0,333…). В Завершают только рациональные числа с базисом 2 со знаменателями со степенями 2 (например, 1/2 или 3/16). Любое рациональное число со знаменателем с простым множителем, отличным от 2, будет иметь бесконечное двоичное разложение. Это означает, что числа которые кажутся короткими и точными при записи в десятичном формате, возможно, потребуется приблизиться при преобразовании в двоичную с плавающей запятой.Например, десятичное число 0.1 не представляется в двоичной с плавающей запятой любой конечной точности; точное двоичное представление будет иметь последовательность «1100», продолжающуюся бесконечно:
е = -4; s = 1100110011001100110011001100110011…, где, как и ранее, s — это значение, а e — показатель степени.
При округлении до 24 бит это становится
е = -4; s = 110011001100110011001101, что на самом деле составляет 0,100000001490116119384765625 в десятичном виде. «
BigDecimal для спасения
BigDecimal представляет десятичное число со знаком произвольной точности с соответствующим масштабом. BigDecimal обеспечивает полный контроль над точностью и округлением числового значения. Фактически можно вычислить значение числа пи до 2 миллиардов знаков после запятой, используя BigDecimal, при этом доступная физическая память является единственным ограничением.
Вот почему мы всегда должны предпочитать BigDecimal или BigInteger для финансовых расчетов.
Особые заметки
Примитивный тип: int и long также полезны для денежных расчетов, если десятичная точность не требуется.
Мы действительно должны избегать использования конструктора BigDecimal (двойное значение), и вместо этого мы действительно должны предпочесть использование BigDecimal (String), потому что BigDecimal (0.1) приводит к (0.1000000000000000055511151231257827021181583404541015625), хранящемуся в экземпляре BigDecimal. Напротив, BigDecimal («0.1») хранит ровно 0.1.
Что такое точность и масштаб?
Точность — это общее количество цифр (или значащих цифр) действительного числа.
Шкала указывает количество цифр после запятой. Например, 12.345 имеет точность 5 (общее количество цифр) и шкалу 3 (количество цифр справа от десятичного числа).
Как мы можем отформатировать значение BigDecimal, не получая в результате возведения в степень и обнулив конечные нули?
Мы можем получить возведение в степень в результате расчета, если мы не будем следовать рекомендациям при использовании BigDecimal
. Ниже приведен фрагмент кода, который показывает полезный пример использования обработки результата вычисления с помощью BigDecimal
.
BigDecimal Округление:
import java.math.BigDecimal;
public class BigDecimalForCurrency {
public static void main(String[] args) {
int scale = 4;
double value = 0.11111;
BigDecimal tempBig = new BigDecimal(Double.toString(value));
tempBig = tempBig.setScale(scale, BigDecimal.ROUND_HALF_EVEN);
String strValue = tempBig.stripTrailingZeros().toPlainString();
System.out.println("tempBig = " + strValue);
}
}
Выход программы
tempBig = 0.1111
Как бы вы напечатали данное значение валюты для индийского языка (INR валюты)?
NumberFormat
Класс разработан специально для этой цели. Символ валюты и режим округления автоматически устанавливаются в зависимости от используемой локали NumberFormat
. Давайте посмотрим на это в действии
Пример NumberFormat
class Scratch {
public static String formatRupees(double value) {
NumberFormat format = NumberFormat.getCurrencyInstance(new Locale("en", "in"));
format.setMinimumFractionDigits(2);
format.setMaximumFractionDigits(5);
format.setRoundingMode(RoundingMode.HALF_EVEN);
return format.format(value);
}
public static void main(String[] args) {
BigDecimal tempBig = new BigDecimal(22.121455);
System.out.println("tempBig = " + formatRupees(tempBig.doubleValue()));
}
}
Выход программы:
tempBig = Rs.22.12146
Вот и все, обо всем позаботятся NumberFormat
.
Меры предосторожности
- Конструктор BigDecimal (String) всегда должен быть предпочтительнее, чем конструктор BigDecimal (Double), потому что использование
BigDecimal(double)
непредсказуемо из-за невозможности двойного представления 0.1 как точного 0.1. - Если для инициализации BigDecimal необходимо использовать double, используйте метод
BigDecimal.valueOf(double)
, который преобразует значение Double в String с помощью методаDouble.toString
(double) - Режим округления должен быть предусмотрен при настройке масштаба
StripTrailingZeros
отсекает все конечные нулиtoString()
может использовать научную нотацию, ноtoPlainString()
никогда не вернет возведения в степень в своем результате
Вы знали?
Использование Float, Double вместо BigDecimal может быть фатальным для военных
25 февраля 1991 года потеря значимости в ракетной батарее MIM-104 Patriot помешала ей перехватить поступающую ракету «Скад» в Дахране, Саудовская Аравия, что привело к гибели 28 солдат из 14-го отряда армии США.
Режим округления банкира
С момента появления IEEE 754 метод по умолчанию (с округлением до ближайшего десятичного знака, связывается с четным и иногда называется округлением Банкира или RoundingMode.HALF_EVEN
) чаще используется в США. Этот метод округляет идеальный (бесконечно точный) результат арифметической операции до ближайшего представимого значения и дает это представление в качестве результата. В случае связи выбирается значение, которое будет означать значение и заканчивается четной цифрой.
Дальнейшее чтение
- https://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal
- Item 60, Effective Java, 3-е издание, Джошуа Блох