Хотите знать, почему типы данных float и double никогда не должны использоваться для вычислений с плавающей запятой? Ознакомьтесь с этим кратким обзором, чтобы узнать больше об использовании BigDecimal для выполнения точных операций.

Введение

Большинство корпоративных приложений оперируют значениями с плавающей запятой.
Финтех, электронная коммерция, финансы и другие приложения ежедневно имеют дело с операциями с плавающей запятой и нуждаются в полном контроле точности для всех вычислений.
Разработчики должны знать, какой тип данных Java подходит для представления плавающих чисел. -точечные значения, особенно денежные значения.
Попробуем разобраться.

Почему двойное не точно

Первое, что приходит на ум, это использовать типы данных float и double.
Однако эти типы данных не рекомендуются, если требуются точные значения. Дело в том, что типы float и double предназначены в первую очередь для научных и инженерных расчетов.
В них реализована двоичная арифметика с плавающей запятой, тщательно разработанная для быстрого получения правильного приближения для широкого диапазона значений.
К сожалению, эти типы не дают точных результатов и поэтому непригодны для денежных расчетов:

Чтобы лучше понять, как представляются числа с плавающей запятой, ознакомьтесь со Стандартом IEEE для арифметики с плавающей запятой.

Как BigDecimal обеспечивает точность

Возвращаясь к введению, давайте посмотрим, как BigDecimal представляет число и как он гарантирует точность.
Глядя на исходный код BigDecimal, мы можем обнаружить, что BigDecimal представлен числом с немасштабированным значением и масштабом:

Поле масштаба представляет масштаб BigDecimal.
Немасштабированные значения используют немного более сложное представление.
Когда немасштабированное значение превышает пороговое значение (по умолчанию — Long.MAX_VALUE), поле intVal используется для хранения значения, а поле intCompact сохраняется. Long.MIN_VALUE, для указания мантиссы информация доступна только из intVal.
В противном случае немасштабированное значение компактно сохраняется в поле intCompact длинного типа для последующего вычисления, а intVal пуст.

BigDecimal также предоставляет метод масштабирования для возврата масштаба BigDecimal:

Комментарий к этому методу дает подробное описание того, как используется значение шкалы:

Возвращает масштаб этого BigDecimal. Если ноль или положительный, масштаб представляет собой количество цифр справа от десятичной точки. Если отрицательное, немасштабированное значение числа умножается на десять в степени отрицания шкалы. Например, масштаб -3 означает, что немасштабированное значение умножается на 1000.

Возвращаясь к приведенному выше примеру, число 0,61, которое не может быть точно представлено в двоичном формате, может быть представлено с помощью BigDecimal с немасштабированным значением 61 и масштабом 2.

Как правильно создать BigDecimal

Глядя на исходный код BigDecimal, мы можем выделить четыре важных конструктора:

Масштаб BigDecimal, созданный четырьмя вышеуказанными конструкторами, отличается. BigDecimal(int) и BigDecimal(long) являются более простыми и принимают целочисленные входные параметры, поэтому их шкалы равны 0.
Масштабы BigDecimal(double) и BigDecimal(String) требуют более подробного рассмотрения.

Что не так с BigDecimal(double)

BigDecimal предоставляет метод для создания BigDecimal из числа double, но использовать его не рекомендуется, поскольку результаты могут быть несколько непредсказуемыми. BigDecimal, созданный с использованием двойного значения 0,61, не совсем равен 0,61, потому что 0,61 не может быть точно представлено как двоичное число конечной длины. Фактическое представление:

Таким образом, использование BigDecimal(double), особенно для вычислений, связанных с финансовыми транзакциями, чрезвычайно опасно, поскольку может привести к потере точности.

Создание с использованием BigDecimal(String)

Конструктор BigDecimal(String) полностью предсказуем. Строка new BigDecimal("0.61") создает BigDecimal, который точно равен 0,61:

Однако следует отметить, что масштабы new BigDecimal("0.610000") и нового BigDecimal("0.61") равны 6 и 2 соответственно. Результат сравнения методом equals этих BigDecimals является ложным.

Соответственно, есть два следующих метода для создания BigDecimal, которые могут точно представлять 0,61:

Метод BigDecimal.valueOf(double) использует двухэтапный процесс и реализуется путем вызова метода Double.toString(double). Первый шаг — преобразовать Double в String. Второй шаг — преобразовать String в BigDecimal:

Заключение

Поскольку компьютеры используют двоичный код для хранения и обработки данных, многие числа с плавающей запятой не могут быть точно представлены в виде двоичной дроби любой конечной длины.
Поэтому способ представления чисел с плавающей запятой в компьютерах был принят через аппроксимации, такие как форматы с плавающей запятой одинарной точности и с плавающей запятой двойной точности.
К сожалению, эти типы не дают точных результатов и непригодны для точных расчетов, особенно в отношении финансовых транзакций.
Соответственно, использование BigDecimal(double) крайне опасно, поскольку может привести к потере точности и непредсказуемым результатам.
Таким образом, обычно предпочтительным и настоятельно рекомендуемым способом создания BigDecimal является использование методов BigDecimal(String) и BigDecimal.valueOf(double).

Рекомендации

[1] Ссылка на Стандарт IEEE для арифметики с плавающей запятой

Первоначально опубликовано на http://argen666.github.io 18 августа 2022 г.