Autoboxing и unboxing в Java
Для Integer a = 1000, b = 1000; выражения a <= b и a >= b возвращают true, а a == b — false. Число одно и то же, но в первых двух сравнениях компилятор распаковал обе обертки в int, а в третьем сравнил ссылки на два разных объекта. Так незаметно работает автоупаковка.
Автоупаковка (autoboxing, боксинг) — это автоматическое преобразование значения примитивного типа в объект соответствующего класса-обертки: int → Integer, double → Double. Автораспаковка, или анбоксинг (unboxing), — это обратное преобразование объекта-обертки в примитив. Обе операции появились в Java 5 и выполняются компилятором: он сам подставляет в код вызовы Integer.valueOf() и intValue().
Что такое автоупаковка и автораспаковка (boxing и unboxing)
Автоупаковка и автораспаковка — это неявные преобразования между примитивом и его оберткой, которые компилятор Java вставляет сам: там, где ожидается Integer, а передан int, значение упаковывается; там, где нужен int, а есть Integer, — распаковывается. Простой пример autoboxing и unboxing в Java:
public class AutoBoxExample1 {
public static void main(String[] args) {
Integer iOb = 100; // автоупаковка int -> Integer
int i = iOb; // автораспаковка Integer -> int
System.out.println(i + " " + iOb);
}
} Результат выполнения:
100 100 Во что компилятор превращает автоупаковку
Упаковку и распаковку выполняет компилятор javac (входит в JDK), а не 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); Убедиться в этом можно, дизассемблировав класс: в байт-коде будут обычные вызовы java/lang/Integer.valueOf и java/lang/Integer.intValue.
javac AutoBoxExample1.java
javap -c AutoBoxExample1 Где происходит автоупаковка и распаковка
Преобразование срабатывает в любом месте, где тип выражения не совпадает с ожидаемым, но является парой примитив–обертка:
| Контекст | Пример | Что делает компилятор |
|---|---|---|
| Присваивание обертке | Integer x = 5; | Integer.valueOf(5) |
| Присваивание примитиву | int n = x; | x.intValue() |
| Аргумент метода | process(5) при process(Integer v) | упаковывает аргумент |
| Возврат из метода | return value; (метод int, value типа Integer) | распаковывает возвращаемое значение |
| Арифметика и сравнения <, >, <=, >= | x + 1, x < y | распаковывает операнды, результат при необходимости упаковывает |
| Условие if, while, for, тернарный оператор | if (flag) при Boolean flag | flag.booleanValue() |
| == между оберткой и примитивом | x == 5 | распаковывает x и сравнивает значения |
| Коллекции и дженерики | list.add(5), int n = list.get(0) | упаковка при добавлении, распаковка при чтении (подробнее — в разделе Обертки в коллекциях и дженериках) |
Автоупаковке могут подвергаться как переменные примитивных типов, так и литералы:
Integer iOb1 = 100; // литерал
int i = 200;
Integer iOb2 = i; // переменная Автоупаковка переменной требует точного соответствия типов: byte упаковывается только в Byte, int — только в Integer. Попытка упаковать переменную byte в Short без явного приведения не скомпилируется (incompatible types: byte cannot be converted to Short):
byte b = 4;
// Short s1 = b; // ошибка компиляции
Short s2 = (short) b; // сначала приведение byte -> short, затем упаковка Неочевидный момент
Short s = 4; и Character c = 65; компилируются: для констант, помещающихся в тип, спецификация (JLS 5.2) разрешает сужение и затем упаковку в Byte, Short или Character. А Long l = 4; и Double d = 1; — ошибка incompatible types: int cannot be converted to Long: расширение int → long с последующей упаковкой не допускается. Пишите Long l = 4L; и Double d = 1.0;.
Это же правило «сначала упаковать, потом расширить можно, наоборот — нельзя» влияет на выбор перегруженного метода; порядок, в котором компилятор пробует расширение, упаковку и var-args, разобран в уроке Перегрузка с дополнительными факторами.
Автоупаковка в методах и выражениях: примеры
При вызове метода аргумент упаковывается, а возвращаемое значение распаковывается или упаковывается в зависимости от типов:
public class AutoBoxAndMethods {
static int someMethod(Integer value) {
return value; // распаковка Integer -> int
}
public static void main(String[] args) {
Integer iOb = someMethod(100); // 100 упаковывается в аргумент, результат int снова упаковывается
System.out.println(iOb); // 100
}
} Внутри выражения обертка автоматически распаковывается, вычисление идет над примитивами, а результат при необходимости упаковывается заново:
public class AutoBoxAndOperations {
public static void main(String[] args) {
Integer iOb1 = 100;
Integer iOb2 = iOb1 + iOb1 / 3; // 100 + 33, результат упаковывается
System.out.println("iOb2 после выражения: " + iOb2);
int i = iOb1 + iOb1 / 3; // результат остается int
System.out.println("i после выражения: " + i);
}
} Результат выполнения (деление целочисленное, поэтому 100 / 3 = 33):
iOb2 после выражения: 133
i после выражения: 133 Объект Boolean можно использовать как условие в операторе if, в циклах while, do-while, for и в тернарном операторе — он распаковывается в boolean. Так же работает Character:
public class AutoBoxAndCharacters {
public static void main(String[] args) {
Boolean b = true;
if (b) { // b.booleanValue()
System.out.println("В if тоже можно использовать распаковку.");
}
Character ch = 'x'; // Character.valueOf('x')
char ch2 = ch; // ch.charValue()
System.out.println("ch2 = " + ch2);
}
} Результат выполнения:
В if тоже можно использовать распаковку.
ch2 = x Как писали до Java 5 и как пишут сейчас
До Java 5 автоупаковки не было: чтобы увеличить значение обертки на единицу, приходилось вручную распаковывать его, менять примитив и создавать новый объект. В старом коде для этого использовался конструктор new Integer(...), который сейчас устарел: deprecated с Java 9 и помечен forRemoval с Java 16 (подробнее — в разделе valueOf() вместо конструкторов).
// Так писали до Java 5 (не используйте в новом коде)
Integer y = new Integer(567); // устаревший конструктор
int x = y.intValue();
x++;
y = new Integer(x); Перепишем тот же пример с автоупаковкой:
public class AutoBoxExample2 {
public static void main(String[] args) {
Integer y = 567; // Integer.valueOf(567)
y++; // y = Integer.valueOf(y.intValue() + 1)
System.out.println("y = " + y);
}
} Результат выполнения:
y = 568 Код стал короче, но по сути делает то же самое: распаковывает, увеличивает и упаковывает в новый объект. Почему новый — в следующем разделе.
Объекты классов-оберток неизменяемые: что происходит при i++
Объекты классов-оберток неизменяемые (immutable): значение внутри Integer нельзя поменять после создания. Поэтому y++ для переменной типа Integer не изменяет старый объект, а присваивает переменной ссылку на другой объект с новым значением. Остальные ссылки на старый объект продолжают видеть старое значение:
public class AutoBoxImmutability {
public static void main(String[] args) {
Integer y = 567;
Integer x = y;
System.out.println(y == x); // true: x и y ссылаются на один объект
y++;
System.out.println(x + " " + y);
System.out.println(y == x); // false: y теперь ссылается на новый объект
}
} Результат выполнения:
true
567 568
false Разберем, что происходит в памяти. После строки
Integer y = 567; переменная y указывает на объект Integer со значением 567:

После y++ в памяти создается еще один объект, и y теперь указывает на него. Старый объект не изменился: если на него больше никто не ссылается, его со временем удалит сборщик мусора.
Integer y = 567;
y++; 
Для небольших значений «новый» объект может оказаться уже существующим объектом из кэша — об этом следующий раздел.
Кэширование объектов: какие значения кэширует valueOf()
Метод valueOf(), который вызывает автоупаковка, не всегда создает новый объект: для небольших значений он возвращает заранее созданный объект из кэша. Для Integer, Short, Byte и Long это диапазон от -128 до 127, для Character — от 0 до 127, для Boolean — две константы. Значение вне диапазона упаковывается в новый объект.
| Класс | Кэшируемые значения | Кто гарантирует |
|---|---|---|
| Boolean | Boolean.TRUE и Boolean.FALSE | спецификация языка |
| Byte | все значения, от -128 до 127 | спецификация языка |
| Short | от -128 до 127 | спецификация языка |
| Integer | от -128 до 127, верхнюю границу можно увеличить параметром JVM -XX:AutoBoxCacheMax | спецификация языка (для -128..127) |
| Long | от -128 до 127 | только реализация JDK (javadoc Long.valueOf() кэш не обещает) |
| Character | от \u0000 до \u007f (от 0 до 127) | спецификация языка |
| Float, Double | не кэшируются | каждая упаковка создает новый объект |
Гарантированные диапазоны перечислены в разделе JLS 5.1.7 Boxing Conversion. Посмотрим, как кэш влияет на сравнение ссылок:
public class AutoBoxExampleCaching {
public static void main(String[] args) {
Integer i1 = 23;
Integer i2 = 23;
System.out.println(i1 == i2); // один объект из кэша
System.out.println(i1.equals(i2));
Integer i3 = 2300;
Integer i4 = 2300;
System.out.println(i3 == i4); // два разных объекта
System.out.println(i3.equals(i4));
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2); // Double не кэшируется
}
} Результат выполнения с настройками JVM по умолчанию:
true
true
false
true
false Устаревший конструктор new Integer(23) кэш не использует и всегда создает новый объект, поэтому new Integer(23) == new Integer(23) дает false даже для маленьких чисел. Автоупаковка же всегда идет через valueOf() и кэш.
Настройка кэша
Параметр -XX:AutoBoxCacheMax=10000 (или системное свойство java.lang.Integer.IntegerCache.high) увеличивает только верхнюю границу кэша и только для Integer; нижняя граница -128 и кэши Short, Long, Character не меняются. Значит, результат i3 == i4 для 2300 зависит от параметров запуска — код, который на это опирается, может по-разному вести себя на разных серверах.
Сравнение оберток: == или equals()
Оператор == между двумя обертками сравнивает ссылки, а не значения, поэтому результат зависит от кэша. Если же один из операндов примитив, обертка распаковывается и сравниваются значения. Метод equals() сравнивает значение и тип. Надежное правило: две обертки сравнивайте через equals() или Objects.equals().
public class WrapperComparison {
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b);
System.out.println(a <= b && a >= b);
System.out.println(a.equals(b));
System.out.println(a == 1000);
System.out.println(a.intValue() == b);
Long l = 1000L;
System.out.println(l == 1000);
System.out.println(l.equals(1000));
}
} Результат выполнения:
false
true
true
true
true
true
false | Выражение | Результат | Почему |
|---|---|---|
| a == b | false | две обертки: сравниваются ссылки, 1000 вне кэша — разные объекты |
| a <= b && a >= b | true | операторы <= и >= не определены для ссылок, обертки распаковываются |
| a.equals(b) | true | сравниваются значения |
| a == 1000 | true | один операнд примитив: a распаковывается |
| a.intValue() == b | true | слева int, поэтому b распаковывается |
| l == 1000 | true | l распаковывается в long, 1000 расширяется до long |
| l.equals(1000) | false | 1000 упаковывается в Integer, а Integer не равен Long |
Для упорядочивания оберток используйте compareTo() или Integer.compare(a, b). Если хотя бы одна из оберток может быть null, Objects.equals(a, b) безопаснее, чем a.equals(b), а сравнение с примитивом (a == 1000) при a == null выбросит исключение — см. следующий раздел.
NullPointerException при распаковке null
Распаковка — это скрытый вызов метода intValue() (или другого xxxValue()) у объекта. Если переменная-обертка равна null, вызывать метод не у чего, и распаковка выбрасывает NullPointerException, хотя в строке кода не видно ни одного вызова. Примитив не может хранить null, поэтому «мягкого» варианта распаковки в языке нет.
public class UnboxingNull {
static Integer findDiscount() {
return null; // скидка не найдена
}
public static void main(String[] args) {
int discount = findDiscount(); // NullPointerException
}
} Начиная с Java 14 (по умолчанию включено с Java 15) сообщение NPE подсказывает, что именно распаковывалось:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "UnboxingNull.findDiscount()" is null Та же ошибка возникает в менее очевидных местах:
- Условие с Boolean.
Boolean enabled = null; if (enabled) { ... }— NPE наbooleanValue(). - Арифметика и сравнение с примитивом.
Integer count = null; count++;илиcount > 0— обертка распаковывается перед операцией. - Тернарный оператор со смешанными типами. Если один операнд
Integer, а другой —int, типом всего выражения становитсяint, и операнд-обертка распаковывается, даже если результат присваивается вInteger:
Integer cached = null;
boolean useCache = true;
// Integer value = useCache ? cached : -1; // NPE: тип выражения int
Integer value = useCache ? cached : Integer.valueOf(-1); // оба операнда Integer
System.out.println(value); // null Короткая версия этого правила с примером для Map есть в уроке Классы обертки в Java.
Как распаковать без риска
Подставляйте значение по умолчанию до распаковки: int n = Objects.requireNonNullElse(count, 0); (Java 9+), int age = map.getOrDefault(key, 0); или Optional.ofNullable(count).orElse(0). Еще надежнее — не допускать null там, где по смыслу всегда есть число: объявите поле или переменную как int.
Производительность: автоупаковка в циклах
Автоупаковка не бесплатна: каждая упаковка значения вне кэша создает новый объект в куче. Объект Integer на 64-битной HotSpot занимает около 16 байт против 4 байт у int, а лишние объекты нагружают сборщик мусора. В обычном коде это незаметно, но в «горячих» циклах и при обработке больших массивов чисел разница бывает многократной.
Типичная ошибка — объявить накопитель как обертку:
public class BoxingInLoop {
public static void main(String[] args) {
Integer boxedSum = 0;
for (int i = 0; i < 10_000; i++) {
boxedSum += i; // boxedSum = Integer.valueOf(boxedSum.intValue() + i)
}
int sum = 0;
for (int i = 0; i < 10_000; i++) {
sum += i; // только арифметика над int
}
System.out.println(boxedSum + " " + sum);
}
} Результат выполнения:
49995000 49995000 Результаты одинаковые, но первый цикл создал 9 984 объекта Integer: промежуточная сумма выходит за пределы кэша (больше 127) уже на 17-й итерации, и с этого момента каждый += упаковывает новое значение. JIT-компилятор иногда убирает такие аллокации (escape analysis), но рассчитывать на это нельзя.
То же происходит в Stream API, если работать с Stream<Integer> вместо специализированного потока:
List<Integer> list = List.of(1, 2, 3, 4);
int s1 = list.stream().reduce(0, Integer::sum); // распаковка и упаковка на каждом шаге
int s2 = list.stream().mapToInt(Integer::intValue).sum(); // IntStream работает с int
System.out.println(s1 + " " + s2); // 10 10 Рекомендации:
- счетчики циклов, суммы и другие накопители объявляйте примитивами (
int,long,double); - для больших наборов чисел используйте массивы
int[]и потокиIntStream,LongStream,DoubleStreamвместоList<Integer>иStream<Integer>; - не смешивайте обертки и примитивы в одном выражении без необходимости: каждое смешение — это распаковка, а присваивание результата обертке — новая упаковка;
- обертки оставляйте там, где они действительно нужны: коллекции, дженерики, значение
nullкак «нет данных».
Часто задаваемые вопросы
Чем boxing отличается от autoboxing?
Boxing (упаковка) — само преобразование примитива в объект-обертку; его можно выполнить явно: Integer.valueOf(5). Autoboxing (автоупаковка) — та же операция, которую компилятор вставляет сам: Integer x = 5;. Аналогично unboxing — это x.intValue(), а автораспаковка — неявный вызов этого метода. В разговорной речи «боксинг» и «анбоксинг» обычно означают именно автоматический вариант.
В какой версии Java появилась автоупаковка?
В Java 5 (J2SE 5.0, 2004 год) — вместе с дженериками, перечислениями enum, var-args и циклом for-each. Новых возможностей от JVM автоупаковка не требует: компилятор превращает ее в обычные вызовы valueOf() и xxxValue(), которые видны в байт-коде.
Почему int[] нельзя присвоить переменной Integer[]?
Автоупаковка работает только для отдельных значений, но не для массивов: int[] и Integer[] — разные, не связанные между собой типы, и компилятор выдаст incompatible types. Преобразовать массив можно через поток: Arrays.stream(arr).boxed().toArray(Integer[]::new). По той же причине Arrays.asList(new int[]{1, 2, 3}) вернет List<int[]> из одного элемента, а не List<Integer>.
Можно ли использовать Integer в switch и что будет при null?
Да, switch принимает Byte, Short, Character и Integer: значение распаковывается перед выбором ветки. Если переменная равна null, классический switch выбросит NullPointerException. Начиная с Java 21 (JEP 441) в switch можно добавить ветку case null и обработать отсутствие значения без исключения.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
- Сравнение Integer и Long: == или equals()
- Long или long: автоупаковка в цикле
- Какой перегруженный метод вызовется: расширение, boxing, varargs
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии