Перейти к содержимому
Теория · Операторы и выражения · урок 5 из 8

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

Калькулятор Java отбрасывает дробь при целом делении, и это одно его правило. Другое касается размера: у каждого целого типа ограниченное число разрядов.

Вспомните счётчик пробега в старой машине. У него шесть колёсиков с цифрами, и больше 999999 он показать не может. Проедьте ещё километр — и счётчик покажет 000000: колёсики провернулись и начали сначала.

С int происходит похожее. В него помещаются числа примерно от минус двух до плюс двух миллиардов. Посмотрим, что будет, если прибавить единицу к самому большому из них:

JAVA
public class Main {
    public static void main(String[] args) {
        int max = Integer.MAX_VALUE;
        System.out.println(max);     // 2147483647
        System.out.println(max + 1); // -2147483648
    }
}

Давайте разберём программу по строкам:

  1. Integer.MAX_VALUE — константа из стандартной библиотеки, самое большое значение int. Программа кладёт его в max.
  2. System.out.println(max) печатает 2147483647.
  3. System.out.println(max + 1) печатает -2147483648: число 2147483648 в int не помещается, и значение перешло на другой конец диапазона.

Ни ошибки компиляции, ни исключения: программа молча напечатала неверное число.

Здесь аналогия со счётчиком расходится с Java. У счётчика нет отрицательных чисел, а у int есть. Поэтому после максимума идёт не ноль, а самое маленькое отрицательное число.

Границы int

Самое маленькое значение int хранит константа Integer.MIN_VALUE, она равна -2147483648. Если результат не помещается в диапазон типа, происходит переполнение. В Java оно молчаливое в обе стороны:

  • после максимума идёт минимум: Integer.MAX_VALUE + 1 равно Integer.MIN_VALUE;
  • ниже минимума идёт максимум: Integer.MIN_VALUE - 1 равно Integer.MAX_VALUE;
  • с long всё так же, только граница дальше: Long.MAX_VALUE равно 9223372036854775807.

Этот вопрос часто задают на собеседовании: что даст Integer.MAX_VALUE + 1? Ответ: Integer.MIN_VALUE, без ошибки и без исключения.

Восемь битов byte

У int 32 бита, и следить сразу за всеми неудобно. Тип byte переполняется по тому же правилу, только битов у него восемь, а диапазон — от -128 до 127.

В каждом бите стоит 0 или 1, а число в byte — это сумма весов тех битов, которые равны единице. Веса идут справа налево: 1, 2, 4 и так до 64. У левого бита вес отрицательный, -128, поэтому его называют знаковым битом: если он равен единице, число отрицательное.

Дальше byte послужит уменьшенной моделью int. Кнопка x++ прибавляет к числу единицу: нажимайте её и следите, что станет со знаковым битом после 127.

Вживуюbyte: восемь битовx = 125

x = 125: в byte восемь битов, самое большое число — 127. Нажмите x++.

Следите за левым битом: его вес −128

В 127 единицы стоят во всех битах, кроме знакового: 0111 1111. Прибавление единицы переносит разряд справа налево до самого левого бита, и получается 1000 0000. Единица осталась только в знаковом бите, поэтому число равно -128. В int всё устроено так же, только знаковый бит весит -2147483648.

b++ и приведение к byte

Кнопка виджета подписана x++ не случайно: так в Java записывают «прибавить к переменной единицу». Давайте повторим то же самое в коде, а заодно посмотрим, что делает приведение к byte:

JAVA
byte b = 127;
b++;
System.out.println(b);          // -128
System.out.println((byte) 200); // -56
  1. b++ увеличивает переменную на единицу. Тип при этом не меняется, поэтому b переходит через границу так же, как в виджете.
  2. (byte) 200 приводит int к byte. От числа остаются только восемь младших битов: 1100 1000. Знаковый бит равен единице, поэтому получается -128 + 64 + 8, то есть -56.

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

Как защититься

Неверное число опаснее остановленной программы: с ним она продолжит считать деньги или время. Защититься можно двумя способами:

JAVA
int max = Integer.MAX_VALUE;
long sum = (long) max + 1;
System.out.println(sum);                   // 2147483648
System.out.println(Math.addExact(max, 1));
TEXT
Exception in thread "main" java.lang.ArithmeticException: integer overflow

Считать в long

В long помещаются числа до 9223372036854775807, поэтому сумма двух int в нём не переполнится. Важно перейти в long до сложения: (long) max + 1 складывает уже два long и печатает 2147483648.

Проверять через Math.addExact

Math.addExact(a, b) возвращает сумму, если она помещается в тип, а иначе бросает ArithmeticException с текстом integer overflow. Для вычитания и умножения есть Math.subtractExact и Math.multiplyExact.

Частая ошибка

Число миллисекунд в 30 днях кладут в переменную long:

JAVA
public class Main {
    public static void main(String[] args) {
        long ms = 30 * 24 * 60 * 60 * 1000; // ✗
        System.out.println(ms);             // -1702967296
    }
}

Переменная ms имеет тип long, но все числа справа — int. Поэтому Java перемножает их в int, произведение 2592000000 переполняется, и только потом неверное число попадает в long. Поставьте L у первого числа: 30L * 24 * 60 * 60 * 1000 считается в long и даёт 2592000000.

Главное

  • Границы int: Integer.MAX_VALUE равно 2147483647, Integer.MIN_VALUE равно -2147483648.
  • Переполнение молчаливое: Integer.MAX_VALUE + 1 даёт Integer.MIN_VALUE без ошибки и без исключения.
  • Минус появляется из-за знакового бита: в byte 127 + 1 даёт 1000 0000, то есть -128.
  • От переполнения защищают переход в long до вычисления, как в 30L * …, и Math.addExact, который бросает ArithmeticException.