Перейти к содержимому
Теория · Обёртки и равенство · урок 3 из 8

Автоупаковка и распаковка

В прошлом уроке монету клали в конверт вручную, через Integer.valueOf(42), а доставали методом intValue(). Писать так на каждом шагу утомительно. Поэтому с Java 5 эту работу делает компилятор: он сам кладёт монету в конверт и сам её достаёт. В программе ниже нет ни одного valueOf и ни одного intValue:

JAVA
public class Main {
    static int twice(Integer x) {
        return x * 2;
    }

    public static void main(String[] args) {
        Integer boxed = 5;
        int back = boxed;
        System.out.println(back + 1);    // 6
        System.out.println(twice(back)); // 10
    }
}

Давайте разберём по строкам:

  1. Integer boxed = 5; — справа число int, а слева переменная Integer. Компилятор сам дописывает вызов: Integer boxed = Integer.valueOf(5);.
  2. int back = boxed; — обратный случай: компилятор превращает строку в int back = boxed.intValue();.
  3. back + 1 — обычное сложение двух int, печатается 6.
  4. twice(back) передаёт int в параметр типа Integer, и компилятор упаковывает аргумент.
  5. Внутри twice выражение x * 2 распаковывает x: умножать можно только числа. Метод возвращает 10.

Упаковка и распаковка

Автоупаковка — автоматическое превращение примитива в объект-обёртку: компилятор вставляет вызов valueOf. Распаковка — обратный путь: компилятор вставляет intValue(), а для других обёрток longValue(), doubleValue() и так далее.

Это происходит везде, где примитив встречается с обёрткой:

  • в присваивании: Integer boxed = 5; и int back = boxed;;
  • при вызове метода: int идёт в параметр Integer или Integer — в параметр int;
  • в арифметике и сравнениях < и >: boxed + 1 и boxed < 10 сначала распаковывают обёртку.

Здесь картинка с конвертом неточна в одном важном месте. Человек видит, что держит в руке конверт, а не монету. В коде упаковки не видно: строка int back = boxed; выглядит как копирование числа, хотя за ней стоит вызов метода.

Распаковка null

Скрытый вызов метода и есть ловушка. Распаковать можно только существующий объект, а переменная обёртки может хранить null:

JAVA
Integer count = null;
int total = count + 1; // ✗
TEXT
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local1>" is null

Программа компилируется, а при запуске останавливается. В строке нет ни одной точки, но компилятор вставил в неё вызов count.intValue(). Конверта нет, и доставать монету неоткуда. Сообщение говорит об этом прямо:

  • Cannot invoke "java.lang.Integer.intValue()" — не удалось вызвать скрытый метод intValue();
  • because "<local1>" is null — потому что в первой локальной переменной, count, лежит null.

Как и в теме о классах, java Main.java не сохраняет имён локальных переменных, поэтому JVM называет count по номеру. Распаковка null никогда не даёт 0: это всегда исключение NullPointerException. На собеседовании эту ловушку любят: откуда NullPointerException в строке, где нет ни одной точки?

Упаковка в цикле

У автоупаковки есть и цена: каждый новый объект Long занимает место в куче. Посмотрите на такую сумму:

JAVA
Long total = 0L;
for (long i = 1; i <= 1_000_000; i++) {
    total += i;
}
System.out.println(total); // 500000500000

Для компилятора строка total += i выглядит так: total = Long.valueOf(total.longValue() + i);. Во время работы программа на каждом круге распаковывает сумму, складывает и упаковывает результат в новый объект. Миллион кругов — около миллиона новых объектов, которые сразу же становятся ненужными.

С long total = 0; тот же цикл не создаёт ни одного объекта и работает заметно быстрее. Правило такое: считают на примитивах, а обёртки берут там, где нужен объект или null.

Частая ошибка

Счётчик посещений объявили полем типа Integer:

JAVA
public class Main {
    static Integer visits;

    public static void main(String[] args) {
        visits++; // ✗
        System.out.println(visits);
    }
}
TEXT
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "Main.visits" is null

Вспомните значения по умолчанию у static-полей: 0 у int, но null у любого ссылочного типа. Integer — ссылочный тип, поэтому в visits лежит null, а visits++ пытается его распаковать. Здесь сообщение называет поле по имени: Main.visits. Счётчику нужен примитив: static int visits; начинается с 0.

Главное

  • Автоупаковка — компилятор сам вызывает Integer.valueOf, распаковка — intValue(); так бывает в присваивании, при вызове метода и в арифметике.
  • Распаковка null бросает NullPointerException, даже если в строке нет ни одной точки.
  • Поле типа Integer по умолчанию равно null, а не 0.
  • Упаковка в цикле создаёт новый объект почти на каждом круге, поэтому считают на примитивах.