Wrapper классы ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-15

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 типа Integer указывает на объект со значением 567 в памяти

После y++ в памяти создается еще один объект, и y теперь указывает на него. Старый объект не изменился: если на него больше никто не ссылается, его со временем удалит сборщик мусора.

Integer y = 567;
y++;

После y++ переменная y указывает на другой объект Integer в памяти

Для небольших значений «новый» объект может оказаться уже существующим объектом из кэша — об этом следующий раздел.

Кэширование объектов: какие значения кэширует 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 и обработать отсутствие значения без исключения.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.