Хотите знать, почему типы данных 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 г.