Autoboxing и unboxing в Java - Вопросы
Всего: 6 вопросов
1. Во что компилятор Java превращает автоупаковку и автораспаковку? Как в этом убедиться?
Во что компилятор 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; — нет?
Почему 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?
Объекты 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() у разных классов-оберток и что из этого гарантирует спецификация?
Автоупаковка вызывает 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 и как распаковать значение безопасно?
В каких неочевидных местах распаковка 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 как «нет данных».