Получить доступ

Примитивы, обёртки и Integer cache

Восемь примитивов и их диапазоны, autoboxing, кэш Integer и ловушка ==, NPE при распаковке и цена боксинга.

Восемь примитивов

В 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/примитивные массивы в горячем коде).

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем