Автобоксинг и анбоксинг (Java 5+)

В Java есть две параллельные «вселенные» числовых типов:

  • Примитивы: byte, short, int, long, float, double, char, boolean — лежат на стеке, не имеют методов, не могут быть null.

  • Обёртки (wrapper classes): Byte, Short, Integer, Long, Float, Double, Character, Boolean — настоящие объекты в куче, могут быть null, нужны для коллекций и generics.

С Java 5 компилятор сам вставляет преобразования между ними:

  • Autoboxing — примитив автоматически заворачивается в обёртку: Integer x = 5;

  • Unboxing — обёртка автоматически разворачивается в примитив: int y = new Integer(5);

Минимальная версия: Java 5.

Синтаксис

Integer boxed = 42;      // autoboxing: int -> Integer
int unboxed = boxed;     // unboxing:   Integer -> int

// Вручную это выглядело бы так:
// Integer boxed = Integer.valueOf(42);
// int unboxed = boxed.intValue();

Зачем нужно

  • Коллекции (List, Map) хранят только объекты — без обёрток List<int> написать нельзя.

  • Generics не работают с примитивами: Optional<int> не существует, нужен Optional<Integer>.

  • Удобство: map.put("speed", 150); без явного Integer.valueOf(150).

Пример 1. Базовый автобоксинг

public class BoxingBasic {
    public static void main(String[] args) {
        // int -> Integer
        Integer boxed = 42;
        // Integer -> int
        int unboxed = boxed;

        System.out.println("boxed:   " + boxed + " (тип Integer)");
        System.out.println("unboxed: " + unboxed + " (тип int)");

        // Арифметика с обёртками: компилятор unbox-ит, считает, box-ит обратно
        Integer a = 10;
        Integer b = 20;
        Integer c = a + b;
        System.out.println("a + b = " + c);
    }
}

Output:

boxed:   42 (тип Integer)
unboxed: 42 (тип int)
a + b = 30

Пример 2. Коллекции работают только с обёртками

import java.util.ArrayList;
import java.util.List;

public class BoxingCollection {
    public static void main(String[] args) {
        // List<int> писать нельзя — только List<Integer>
        List<Integer> pins = new ArrayList<>();

        // Кладём int — компилятор автоматически бокcит
        pins.add(3);
        pins.add(7);
        pins.add(9);

        int sum = 0;
        for (int p : pins) {  // здесь unboxing на каждом шаге
            sum += p;
        }
        System.out.println("Пины: " + pins);
        System.out.println("Сумма: " + sum);
    }
}

Output:

Пины: [3, 7, 9]
Сумма: 19

Пример 3. Integer cache (-128..127)

public class BoxingCache {
    public static void main(String[] args) {
        // В диапазоне -128..127 Integer.valueOf возвращает кэшированный объект
        Integer a = 100;
        Integer b = 100;
        System.out.println("100 == 100: " + (a == b));     // true (один объект)

        Integer c = 200;
        Integer d = 200;
        System.out.println("200 == 200: " + (c == d));     // false (разные объекты)

        // Правильный способ — всегда equals
        System.out.println("equals(200): " + c.equals(d)); // true
    }
}

Output:

100 == 100: true
200 == 200: false
equals(200): true

Пример 4. NullPointerException при unboxing null

public class BoxingNpe {
    public static void main(String[] args) {
        Integer x = null;

        try {
            int y = x;   // unboxing null -> NPE
            System.out.println(y);
        } catch (NullPointerException e) {
            System.out.println("Поймали NPE при разворачивании null");
        }

        // Часто незаметная ловушка — арифметика
        Integer count = null;
        try {
            int z = count + 1;  // тоже NPE
        } catch (NullPointerException e) {
            System.out.println("Снова NPE при count + 1");
        }
    }
}

Output:

Поймали NPE при разворачивании null
Снова NPE при count + 1

Пример 5. Производительность — лишний боксинг в цикле

public class BoxingPerf {
    public static void main(String[] args) {
        // ПЛОХО: Long в цикле — миллион ненужных боксингов
        long t1 = System.nanoTime();
        Long sumBoxed = 0L;
        for (long i = 0; i < 1_000_000L; i++) {
            sumBoxed += i; // unbox, +, box на каждой итерации
        }
        long t2 = System.nanoTime();

        // ХОРОШО: примитив
        long t3 = System.nanoTime();
        long sumPrim = 0L;
        for (long i = 0; i < 1_000_000L; i++) {
            sumPrim += i;
        }
        long t4 = System.nanoTime();

        System.out.println("Сумма (Long):  " + sumBoxed);
        System.out.println("Сумма (long):  " + sumPrim);
        System.out.println("Long  ~ " + (t2 - t1) / 1_000_000 + " мс");
        System.out.println("long  ~ " + (t4 - t3) / 1_000_000 + " мс");
    }
}

Output (значения времени зависят от железа):

Сумма (Long):  499999500000
Сумма (long):  499999500000
Long  ~ 15 мс
long  ~ 2 мс

Пример 6. Все обёртки и их примитивы

public class BoxingAll {
    public static void main(String[] args) {
        Byte      b = (byte) 10;
        Short     s = (short) 20;
        Integer   i = 30;
        Long      l = 40L;
        Float     f = 1.5f;
        Double    d = 2.5;
        Character c = 'A';
        Boolean   z = true;

        System.out.println("Byte:      " + b + " (" + Byte.SIZE + " бит)");
        System.out.println("Short:     " + s + " (" + Short.SIZE + " бит)");
        System.out.println("Integer:   " + i + " (" + Integer.SIZE + " бит)");
        System.out.println("Long:      " + l + " (" + Long.SIZE + " бит)");
        System.out.println("Float:     " + f);
        System.out.println("Double:    " + d);
        System.out.println("Character: " + c + " (код " + (int) c + ")");
        System.out.println("Boolean:   " + z);
    }
}

Output:

Byte:      10 (8 бит)
Short:     20 (16 бит)
Integer:   30 (32 бит)
Long:      40 (64 бит)
Float:     1.5
Double:    2.5
Character: A (код 65)
Boolean:   true

Подводные камни

Предупреждение

  • Сравнение через ``==`` для обёрток сравнивает ссылки, а не значения. Используйте equals(). Кэш Integer (-128..127) делает ошибку незаметной на малых числах и явной на больших.

  • Unboxing null → NPE. Это самая частая «скрытая» ошибка: Integer x = map.get("k"); int y = x; падает, если ключа нет.

  • Производительность: в горячих циклах автобоксинг создаёт миллионы лишних объектов и грузит GC. Для числовых стримов есть специализации: IntStream, LongStream, DoubleStream.

  • Generics не дружат с примитивами: нельзя List<int>, только List<Integer>. Каждое добавление и чтение — это box/unbox.

  • Тернарный оператор cond ? intValue : null может неожиданно бросить NPE из-за единого типа выражения.

Совет

Правило: примитивы — для расчётов и циклов, обёртки — для коллекций, generic-API и значений, которые могут быть «отсутствующими» (вместе с Optional).

См. также

Примечание

Материал основан на официальной документации Oracle Java SE (docs.oracle.com/en/java/javase), распространяемой под лицензией Oracle Free Documentation License. Тексты и примеры написаны заново для AlashEd Wiki.