Работа с числовыми данными

Целочисленная арифметика

Операторы +, -, *, /, % работают с целыми типами. Умножение, деление и остаток выполняются раньше сложения и вычитания; скобки явно фиксируют намерение.

int pages = 10 / 3;      // 3
int remainder = 10 % 3;  // 1
double exact = 10 / 3.0; // 3.333...

Если оба операнда целые, деление остаётся целочисленным. Приведение результата после деления уже не вернёт потерянную дробную часть:

double wrong = (double) (10 / 3); // 3.0
double right = (double) 10 / 3;   // 3.333...

Остаток в Java сохраняет знак делимого: -5 % 3 == -2. Для математического неотрицательного остатка при положительном модуле можно использовать Math.floorMod.

Переполнение

Обычная целочисленная арифметика переполняется по модулю разрядности и не выбрасывает исключение:

int max = Integer.MAX_VALUE;
int overflow = max + 1; // Integer.MIN_VALUE

Когда переполнение нарушает бизнес-инвариант, применяют Math.addExact, subtractExact, multiplyExact, incrementExact, toIntExact.

int total = Math.addExact(left, right);

Для больших целых без фиксированного диапазона есть BigInteger.

float и double

float и double используют двоичный формат IEEE 754. Многие десятичные дроби в нём непредставимы точно:

double value = 0.1 + 0.2;
System.out.println(value); // 0.30000000000000004

Для измерений сравнивают с допустимой погрешностью:

boolean close = Math.abs(actual - expected) < 1e-9;

Особые значения:

  • NaN не равен даже самому себе; проверка — Double.isNaN;
  • положительная и отрицательная бесконечность;
  • 0.0 и -0.0, которые ведут себя одинаково не во всех операциях.

Нельзя проверять x == Double.NaN.

BigDecimal для десятичной точности

Деньги и другие точные десятичные величины обычно представляют BigDecimal.

BigDecimal price = new BigDecimal("19.90");
BigDecimal quantity = BigDecimal.valueOf(3);
BigDecimal total = price.multiply(quantity);

Создавать BigDecimal из строкового литерала или через valueOf(double) безопаснее, чем new BigDecimal(0.1), который зафиксирует двоичную погрешность.

Операции возвращают новый объект:

BigDecimal amount = new BigDecimal("10.00");
amount = amount.add(new BigDecimal("2.50"));

Деление может потребовать режим округления:

BigDecimal third = BigDecimal.ONE.divide(
        BigDecimal.valueOf(3),
        2,
        RoundingMode.HALF_UP
); // 0.33

equals сравнивает и значение, и scale: 10.0 не равно 10.00. compareTo сравнивает числовое значение и вернёт 0.

Распределение суммы

Если общую сумму нужно разделить на части и сохранить точное равенство, нельзя независимо округлять каждую долю: сумма округлённых частей может отличиться. Один рабочий подход — рассчитывать первые n - 1 частей по принятому правилу, а последнюю определить как остаток от исходной суммы.

Math и сравнение

Math содержит функции abs, min, max, sqrt, pow, округление и точные целочисленные операции. Для сравнения целых в компараторе используйте Integer.compare/Long.compare, а не вычитание, которое может переполниться.

Comparator<Item> byCount =
        (a, b) -> Integer.compare(a.count(), b.count());

Ловушки

  • Целочисленное деление до приведения к double.
  • Тихое переполнение int и long.
  • Денежные вычисления через double.
  • new BigDecimal(double) с неточным двоичным значением.
  • Деление BigDecimal без явной точности и округления.
  • Использование BigDecimal.equals там, где нужна числовая эквивалентность.

Вопросы на собеседовании

  1. Почему 10 / 3 и 10 / 3.0 дают разные результаты?
  2. Что происходит при переполнении int и как его обнаружить?
  3. Почему 0.1 + 0.2 не равно точно 0.3?
  4. Как корректно создать BigDecimal для денежного значения?
  5. Чем BigDecimal.equals отличается от compareTo?
  6. Как разделить сумму на части, сохранив точный итог?

Связанные заметки

Переменные и типы данных Java · Преобразование типов Java · Классы обертки Java