Autoboxing и unboxing в Java - Вопросы

Всего: 6 вопросов

1. 

Во что компилятор Java превращает автоупаковку и автораспаковку? Как в этом убедиться?

Автоупаковку и автораспаковку (появились в Java 5) выполняет компилятор javac, а не JVM во время выполнения. Для упаковки он подставляет вызов статического метода valueOf(), для распаковки — intValue(), doubleValue(), booleanValue() и другие методы xxxValue():

// Что вы пишете
Integer iOb = 100;
int i = iOb;
iOb++;
// Что генерирует компилятор
Integer iOb = Integer.valueOf(100);
int i = iOb.intValue();
iOb = Integer.valueOf(iOb.intValue() + 1);

Преобразование срабатывает везде, где ожидается обертка, а передан примитив (или наоборот): при присваивании, передаче аргумента и возврате из метода, в арифметике и сравнениях <, >, в условиях if/while, в тернарном операторе, при == обертки с примитивом.

Проверить можно дизассемблером: после javac команда javap -c ИмяКласса покажет в байт-коде обычные вызовы java/lang/Integer.valueOf и java/lang/Integer.intValue.

2. 

Почему Short s = 4; компилируется, а Long l = 4; — нет?

Литерал 4 имеет тип int. Для констант, которые помещаются в целевой тип, спецификация (JLS 5.2) разрешает сужение и затем упаковку в Byte, Short или Character, поэтому Short s = 4; и Character c = 65; компилируются.

Расширение int → long с последующей упаковкой в Long не допускается: Long l = 4; и Double d = 1; дают ошибку incompatible types: int cannot be converted to Long. Правильно — Long l = 4L; и Double d = 1.0;.

Для переменных правило еще строже: тип должен точно совпадать с парой примитив–обертка. byte b = 4; Short s = b; не скомпилируется, нужно явное приведение Short s = (short) b;. Упаковать и затем расширить до супертипа можно: Object o = 4; компилируется (int → Integer → Object).

3. 

Объекты Integer неизменяемые. Что тогда происходит при i++ для переменной типа Integer?

Значение внутри объекта-обертки нельзя изменить после создания, поэтому i++ не меняет старый объект. Компилятор превращает его в i = Integer.valueOf(i.intValue() + 1): значение распаковывается, увеличивается и упаковывается, а переменной присваивается ссылка на другой объект.

Остальные ссылки на старый объект продолжают видеть старое значение:

Integer y = 567;
Integer x = y;
System.out.println(y == x);      // true: один объект
y++;
System.out.println(x + " " + y); // 567 568
System.out.println(y == x);      // false: y ссылается на новый объект

Если на старый объект больше никто не ссылается, его удалит сборщик мусора. Для значений от -128 до 127 «новый» объект берется из кэша valueOf().

4. 

Какие значения кэширует valueOf() у разных классов-оберток и что из этого гарантирует спецификация?

Автоупаковка вызывает valueOf(), который для небольших значений возвращает заранее созданный объект из кэша:

  • Boolean — две константы Boolean.TRUE и Boolean.FALSE;
  • Byte — все значения (от -128 до 127);
  • Short и Integer — от -128 до 127;
  • Character — от \u0000 до \u007f (0–127);
  • Long — от -128 до 127, но это только реализация JDK, javadoc кэш не обещает;
  • Float и Double не кэшируются: Double d1 = 1.0, d2 = 1.0; дает d1 == d2 → false.

Диапазоны для Boolean, Byte, Short, Integer и Character гарантирует JLS 5.1.7. Верхнюю границу кэша можно увеличить параметром -XX:AutoBoxCacheMax, но только для Integer, поэтому код, который полагается на == для оберток, может вести себя по-разному на разных серверах. Устаревший конструктор new Integer(...) кэш не использует.

5. 

В каких неочевидных местах распаковка null выбрасывает NullPointerException и как распаковать значение безопасно?

Распаковка — скрытый вызов intValue() или другого xxxValue(), поэтому при null она бросает NPE даже там, где в строке не видно ни одного вызова метода:

  • присваивание примитиву результата метода, вернувшего null: int discount = findDiscount();;
  • условие с Boolean: Boolean enabled = null; if (enabled) { ... };
  • арифметика и сравнение с примитивом: count++, count > 0;
  • тернарный оператор со смешанными типами: в useCache ? cached : -1 тип выражения int, и cached распаковывается, даже если результат присваивается в Integer.

Начиная с Java 14 сообщение NPE подсказывает, что распаковывалось: Cannot invoke "java.lang.Integer.intValue()" because ... is null.

Безопасные варианты: подставить значение по умолчанию до распаковки — Objects.requireNonNullElse(count, 0), map.getOrDefault(key, 0), Optional.ofNullable(count).orElse(0); в тернарном операторе привести оба операнда к Integer; а там, где число есть всегда, объявлять переменную как int.

6. 

Почему автоупаковка в цикле снижает производительность и как этого избежать?

Каждая упаковка значения вне кэша создает новый объект в куче: Integer на 64-битной HotSpot занимает около 16 байт против 4 байт у int, а лишние объекты нагружают сборщик мусора. Типичная ошибка — накопитель-обертка:

Integer boxedSum = 0;
for (int i = 0; i < 10_000; i++) {
    boxedSum += i; // boxedSum = Integer.valueOf(boxedSum.intValue() + i)
}

Результат тот же, что с int sum, но цикл создает 9 984 объекта Integer: сумма выходит за пределы кэша уже на 17-й итерации. JIT иногда убирает такие аллокации (escape analysis), но рассчитывать на это нельзя. То же происходит в Stream<Integer> с reduce(0, Integer::sum).

Как избежать: счетчики и суммы объявлять примитивами (int, long, double); для больших наборов чисел использовать int[] и IntStream/LongStream (например, mapToInt(Integer::intValue).sum()); не смешивать обертки и примитивы в одном выражении без необходимости; обертки оставлять для коллекций, дженериков и null как «нет данных».

Страница 1 из 1