Примитивы и обёртки: int vs Integer

«Что выведет этот код?» — спрашивают на собеседовании и показывают строчку с int и Integer. Если ты не понимаешь разницу между примитивом и объектом-обёрткой, тут легко попасться.

Автоупаковка (autoboxing) — это когда Java сама, незаметно для тебя, превращает примитив в объект-обёртку (например, int в Integer) или наоборот. Удобно, но иногда дорого и неожиданно.

Вопрос-крючок

Смотри на код и попробуй угадать, что будет в консоли:

int a = 10;
Integer b = 10;

System.out.println(a == b);        // ?
System.out.println(a + 5);         // ?

Integer c = null;
int d = c;                         // ?

Вывод:

true
15
Exception in thread "main" java.lang.NullPointerException

Первая строка сравнения — true, потому что при сравнении int и Integer через == Java сначала «распаковывает» Integer обратно в int (это называется unboxing) и сравнивает уже два числа. Вторая строка — обычная арифметика, тут вопросов нет. А вот третья строка взрывается: Integer c = null — это законная переменная (объект может быть null), но когда ты пытаешься положить null в примитивный int, Java пытается сделать unboxing и вызвать у null метод intValue() — а вызывать методы у ничего нельзя. Получаем NullPointerException прямо на присваивании, без единого явного вызова метода в этой строке. Это одна из любимых ловушек на собеседованиях: она выглядит безобидно.

Чем int отличается от Integer

int — это примитивный тип. Он живёт прямо там, где объявлен (в стеке, если это локальная переменная), занимает фиксированные 4 байта и хранит просто число — без всякой обвязки. У примитива нет методов, его нельзя положить в null, и у него нет «личности» — два int с одинаковым значением — это буквально одно и то же число.

Integer — это класс-обёртка (wrapper class). Внутри него всё равно лежит обычный int, но снаружи это полноценный объект: он живёт в куче (heap), у него есть методы вроде .intValue() или .compareTo(), он может быть null, и — самое важное — он может участвовать в коллекциях. Это ключевая причина, почему обёртки вообще нужны: класс ArrayList<int> написать нельзя, а ArrayList<Integer> — можно, потому что дженерики Java умеют работать только с объектами, не с примитивами.

У каждого примитива есть своя обёртка: intInteger, doubleDouble, booleanBoolean, charCharacter и так далее.

Где стреляет автоупаковка

List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
    numbers.add(i);   // каждый int упаковывается в новый Integer
}

Метод add() у List<Integer> принимает объект, а не примитив. Значит, на каждой итерации Java молча вызывает Integer.valueOf(i) и заворачивает число в объект — это и есть автоупаковка. Для миллиона чисел это миллион объектов в куче, лишняя работа для сборщика мусора и заметное замедление по сравнению с массивом int[], где числа лежат впритык друг к другу без всякой обёртки. На собеседовании часто спрашивают именно об этом: почему int[] эффективнее по памяти, чем ArrayList<Integer>. Ответ — из-за обёрток: каждый элемент ArrayList<Integer> — это отдельный объект со своим служебным заголовком, а не просто 4 байта числа.

Обратный процесс — unboxing — происходит, когда объект-обёртку используют там, где ожидается примитив: в арифметике, в условиях if, при присваивании в int. Именно unboxing null-значения и уронил программу в примере выше.

Как это работает под капотом

Компилятор Java расставляет вызовы valueOf() и intValue() автоматически — ты их не видишь в исходном коде, но они есть в скомпилированном байткоде. То есть строчка Integer b = 10; на самом деле превращается в Integer b = Integer.valueOf(10);. Это важно понимать: автоупаковка — это просто удобный синтаксис поверх обычных методов, никакой магии компилятора не происходит, просто он экономит тебе нажатия клавиш.

Частые ошибки на собеседовании

Забывают про NullPointerException при unboxing. Если метод может вернуть Integer (например, значение из Map, которого может не быть), а ты сразу присваиваешь результат в int, программа упадёт при отсутствующем ключе.

Путают эффективность коллекций и массивов. Считают, что ArrayList<Integer> работает так же быстро и компактно, как int[] — нет, из-за упаковки каждого элемента в объект.

Не могут объяснить, зачем вообще нужны обёртки, если есть примитивы. Ответ: дженерики (List<T>), возможность null как «значения нет», и методы вроде Integer.parseInt() для преобразований.

Итоги-шпаргалка

  • int — примитив: живёт в стеке, не может быть null, нет методов, быстрый и компактный.
  • Integer — объект-обёртка: живёт в куче, может быть null, есть методы, нужен для дженериков и коллекций.
  • Автоупаковка (boxing) — примитив в объект; распаковка (unboxing) — объект в примитив. Java делает это сама, невидимо.
  • Unboxing null роняет программу с NullPointerException — частая ловушка на собеседовании.
  • Массовая упаковка (миллионы элементов в ArrayList<Integer>) — заметный удар по памяти и производительности по сравнению с int[].
Проверьте себя
1. Что выведет: int a = 10; Integer b = 10; System.out.println(a == b);
Atrue, потому что Integer распаковывается в int при сравнении
Bfalse, потому что a и b — разные объекты
CОшибка компиляции — нельзя сравнивать int и Integer
Dtrue только для чисел от -128 до 127, иначе false
2. Integer x = null; int y = x; — что произойдёт?
Ay станет равен 0
BОшибка компиляции
CNullPointerException при попытке распаковать null
Dy станет равен null