Восемь примитивов
В Java восемь примитивных типов: целые byte (8 бит), short (16), int (32), long (64); вещественные float (32) и double (64); char (16-битный беззнаковый — UTF-16 code unit) и boolean (размер не специфицирован спецификацией). Примитивы хранят значение напрямую, не объект.
Целочисленное переполнение не бросает исключение, а молча «заворачивается» (Integer.MAX_VALUE + 1 = Integer.MIN_VALUE) — частый источник багов; для контроля есть Math.addExact. Вещественные float/double — это IEEE 754 с погрешностью представления, поэтому 0.1 + 0.2 != 0.3, и для денег берут BigDecimal, а не double.
Обёртки и autoboxing
У каждого примитива есть класс-обёртка (int↔Integer, long↔Long, …), нужный там, где требуется объект: дженерики и коллекции работают только со ссылочными типами, поэтому List<int> невозможен, только List<Integer>.
Autoboxing/unboxing — автоматическое преобразование примитив↔обёртка, которое вставляет компилятор. Удобно, но скрывает аллокации и распаковки, что важно помнить в горячем коде.
Кэш Integer и ловушка ==
Integer.valueOf (а значит и autoboxing) кэширует объекты для значений -128..127: одинаковые маленькие значения дают одну и ту же ссылку. Поэтому Integer a = 100, b = 100; a == b — true, а для 1000 — уже false (разные объекты).
Почему именно -128..127? Кэшировать все 4 млрд значений int невозможно, а вот маленькие значения автобоксятся чаще всего (счётчики циклов, флаги, малые индексы), поэтому кэш именно их даёт почти весь выигрыш по аллокациям при копеечной памяти.
Что именно гарантирует JLS (§5.1.7): Boolean кэшируется целиком (оба значения), Byte — все 256 значений, а Short, Integer и Long — диапазон -128..127, Character — 0..127. Для Integer нижняя граница -128 фиксирована, а верхнюю можно поднять флагом -XX:AutoBoxCacheMax. Float и Double не кэшируются никогда.
Вывод железный: обёртки сравнивают через equals, а не ==. == на обёртках сравнивает ссылки и «случайно» работает только в диапазоне кэша — это классическая ловушка собеседований.
Integer a = 100, b = 100;
System.out.println(a == b); // true (из кэша -128..127)
Integer c = 1000, d = 1000;
System.out.println(c == d); // false (разные объекты)
System.out.println(c.equals(d)); // true — правильное сравнение
NPE при распаковке
Обёртка может быть null, а примитив — нет. Распаковка null-обёртки в примитив бросает NullPointerException: Integer x = null; int y = x; упадёт.
Особенно коварен тернарный оператор со смешением обёртки и примитива (cond ? intValue : integerValue): из-за числового расширения тип всего выражения становится примитивным, поэтому обёрточную ветвь распаковывает — и если выбрана именно она со значением null, будет NPE, хотя выражение выглядит безопасным.
Как бороться: (1) использовать примитив в поле/сигнатуре там, где «отсутствие» бессмысленно — тогда null невозможен в принципе; (2) не смешивать обёртку и примитив в одном арифметическом/тернарном выражении; (3) проверять на null перед распаковкой результатов, способных её вернуть (map.get(key), который при отсутствии ключа даёт null), и подставлять дефолт через Objects.requireNonNullElse или getOrDefault; (4) для «значение может отсутствовать» возвращать Optional, а не nullable-обёртку — это переносит проверку в тип.
Цена боксинга
Каждый бокс — это аллокация объекта в куче и лишняя косвенность при доступе. В циклах и больших структурах это бьёт и по памяти, и по скорости (плюс нагрузка на GC).
Масштаб разницы: int[] на миллион элементов — это сплошной блок ~4 МБ, а List<Integer> на тот же миллион хранит миллион отдельных объектов Integer (каждый с заголовком ~16 байт) плюс массив ссылок — в разы больше памяти и сплошные промахи кэша при обходе.
Поэтому в перфо-чувствительном коде берут примитивные массивы вместо List<Integer> и специализированные потоки (IntStream/LongStream/DoubleStream) вместо Stream<Integer> — они не боксят элементы.
int[] data = new int[1_000_000]; // ~4 МБ, без боксинга, плотно в памяти
long sum = IntStream.of(data).sum(); // примитивный поток, без Integer
List<Integer> boxed = new ArrayList<>(); // каждый элемент — объект Integer
boxed.add(1); // autoboxing на каждой вставке
Как отвечать на собеседовании
Назовите примитивы и их размеры, объясните autoboxing и почему обёртки сравнивают через equals (кэш Integer -128..127 и ловушка ==), предупредите про NPE при распаковке (включая тернарник) и про цену боксинга (аллокации, IntStream/примитивные массивы в горячем коде).