Статьи
YouTube-канал

Советы по работе с BigDecimal в Java

Видеогайд

20 июля 2022

Тэги: Java, руководство.

Содержание

  1. Инициализация через строку
  2. Основные операции
  3. Указывайте точность при делении
  4. Явно указывайте точность результата
  5. Сравнивайте два значения с помощью метода compareTo()
  6. Преобразование в целое число с проверкой
  7. Преобразуйте в строку с помощью toPlainString()
  8. Итоги

Если вы работаете на Java с дробными числами и при этом вам очень важно не потерять в точности, то использовать примитивные типы данных float и double нельзя. Они эту точность не гарантируют, и определяя число 1.01 вы на самом деле получите длинную дробную часть. Это связано с особенностями представления чисел с плавающей точкой.

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

Инициализация через строку

Вы можете создать экземпляр класса BigDecimal на основании числа или строки. При этом использовать значения float и double в качестве исходных опять-таки не рекомендуется.

System.out.println(new BigDecimal(10)); // 10
System.out.println(new BigDecimal("10.1")); // 10.1

System.out.println(new BigDecimal(10.1));
// 10.0999999999999996447286321199499070644378662109375

Инициализация с помощью целых чисел не позволяет вам указать дробные значения. Поэтому универсальным способом инициализации BigDecimal является именно строка. Тогда вы получите ровно то число и с тем количеством знаков, которое вы указали.

Основные операции

Поскольку BigDecimal не является встроенным типом, то обычные операторы вроде «+» и «-» к нему уже не применимы. Вместо этого вам нужно использовать соответствующие методы add(), subtract(), multiply() и divide() соответственно.

var a = new BigDecimal("6.000");
var b = new BigDecimal("3.0");
System.out.println(a.add(b)); // 9.000
System.out.println(a.subtract(b)); // 3.000
System.out.println(a.multiply(b)); // 18.0000
System.out.println(a.divide(b)); // 2.00

Как уже говорилось выше, любые операции над a и b порождают новый экземпляр BigDecimal. При этом исходные экземпляры никак не меняются.

Указывайте точность при делении

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

var a = new BigDecimal("10.0");
var b = new BigDecimal("3.0");
System.out.println(a.divide(b, 4, RoundingMode.HALF_UP)); // явно указали точность - получим 3.3333
System.out.println(a.divide(b)); // не указали точность - получим ArithmeticException

Правильно будет всегда явно указывать количество знаков после запятой (в нашем случае 4 знака) и способ округления RoundingMode. Есть несколько разных способов округления, HALF_UP означает, что если на конце стоит 5, то округляем в большую сторону. То есть 0.05 округляется до 0.1, а 0.04 станет 0.0.

Явно указывайте точность результата

После выполнения последовательности математических операций нелишним будет сделать явное округление. Например, перед сохранением в БД. Так как даже если вы перемножаете два целых числа, у которых есть разная дробная часть, то в результате вы получите ещё бОльшую дробную часть. Ниже приведены варианты с округлением и без.

var a = new BigDecimal("10.00"); // 2 знака после запятой
var b = new BigDecimal("20.000"); // 3 знака
System.out.println(a.multiply(b)); // 200.00000 - 5 знаков
System.out.println(a.multiply(b).setScale(2, RoundingMode.HALF_UP)); // 200.00 - 2 знака

Поэтому явно указывайте точность результата с помощью метода setScale(). И опять-таки этот метод не меняет исходное значение, а порождает новый экземпляр класса.

Сравнивайте два значения с помощью метода compareTo()

В Java объекты на равенство принято сравнивать с помощью метода equals(). Однако в случае с BigDecimal так делать не стоит, поскольку equals() не учитывает точность. Для него числа 10 и 10.0 не равны. Для этих целей используйте метод compareTo(), который в случае равенства возвращает 0.

var a = new BigDecimal("10");
var b = new BigDecimal("10.0");
System.out.println(a.equals(b)); // false
System.out.println(a.compareTo(b) == 0); // true

Метод compareTo() позволяет проверять не только равенство, но и те случаи, когда одно значение больше или меньше другого. Если a > b - метод вернёт 1. Если a < b - метод вернёт -1. На первый взгляд это кажется неочевидным. Однако предлагаю такое эмпирическое правило: тот знак, который вы хотели бы поставить между a и b, ставьте между результатом compareTo() и нулём.

var a = new BigDecimal("2.5");
var b = new BigDecimal("3");
System.out.println(a.compareTo(b) == 0); // проверяем условие a == b, false
System.out.println(a.compareTo(b) > 0); // a > b, false
System.out.println(a.compareTo(b) < 0); // a < b, true
System.out.println(a.compareTo(b) >= 0); // a >= b, false
System.out.println(a.compareTo(b) <= 0); // a <= b, true

Преобразование в целое число с проверкой

Иногда может потребоваться преобразование в целое число из BigDecimal в int или long. Класс предоставляет два метода: intValue() и intValueExact(). Первый просто отбрасывает дробную часть - фактически, округляет «вниз». Второй метод вернёт целое число только если нет дробной части. Если же она имеется, то кинет исключение ArithmeticException.

var a = new BigDecimal("100.00");
System.out.println(a.intValue()); // 100
System.out.println(a.intValueExact()); // 100

var b = new BigDecimal("200.5");
System.out.println(b.intValue()); // 200
System.out.println(b.intValueExact()); // ArithmeticException

Выбирайте один из двух подходов в зависимости от ваших требований. Но скорее всего для контроля точности вы будете использовать intValueExact(). Такая же пара методов имеется и для других целочисленных типов (byte, short, long и даже BigInteger).

Преобразуйте в строку с помощью toPlainString()

Чтобы получить текстовое представление объекта, в Java обычно используют метод toString(). Этот же метод используется по умолчанию при выводе значения в консоль или в лог. Однако в BigDecimal для этих целей лучше использовать метод toPlainString(). Чем же он отличается от стандартного? В большинстве случае текстовое представления, возвращаемое этими методами совпадает. Но есть и различия. Рассмотрим пример.

var a = new BigDecimal("100.01");
System.out.println(a); // неявно вызывается toString(), 100.01
System.out.println(a.toPlainString()); // 100.01

var b = new BigDecimal("0.0000000001");
System.out.println(b); // toString() вернёт 1E-10
System.out.println(b.toPlainString()); // 0.0000000001

Когда значение очень маленькое и содержит большое количество ведущих нулей в дробной части, toString() возвращает «инженерное» представление числа «1E-10», что означает 10 в степени -10. Такой формат может сбить с толку, если вы отображаете это где-то на интерфейсе. Поэтому всегда используйте toPlainString(), чтобы исключить такую ситуацию.

Итоги

Мы рассмотрели несколько полезных советов при работе с классом BigDecimal. Этот класс универсален и позволяет хранить любые значения без ограничения на размер как целой, так и дробной части. При этом он расходует больше памяти, т.к. не является примитивом.

Инициализацию BigDecimal следует делать через текстовое представление числа. Любая операция над BigDecimal порождает новый экземпляр класса, а старый остаётся без изменений. При совершении математических операций нельзя использовать привычные операторы. Вместо них нужно использовать соответствующие методы. Кроме того, имеет смысл явно указывать желаемую точность результата, а при делении это делать обязательно. Для сравнения двух значений также нужно использовать специальный метод compareTo().

Если у Вас остались вопросы по работе с BigDecimal - смело пишите их в комментариях.


Облако тэгов

Kotlin, Java, Java 16, Java 11, Java 10, Java 9, Java 8, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

Добавить комментарий

×

devmark.ru