Автоупаковка и распаковка
В прошлом уроке монету клали в конверт вручную, через Integer.valueOf(42), а доставали методом intValue(). Писать так на каждом шагу утомительно. Поэтому с Java 5 эту работу делает компилятор: он сам кладёт монету в конверт и сам её достаёт. В программе ниже нет ни одного valueOf и ни одного intValue:
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
}
}Давайте разберём по строкам:
Integer boxed = 5;— справа числоint, а слева переменнаяInteger. Компилятор сам дописывает вызов:Integer boxed = Integer.valueOf(5);.int back = boxed;— обратный случай: компилятор превращает строку вint back = boxed.intValue();.back + 1— обычное сложение двухint, печатается6.twice(back)передаётintв параметр типаInteger, и компилятор упаковывает аргумент.- Внутри
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:
Integer count = null;
int total = count + 1; // ✗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 занимает место в куче. Посмотрите на такую сумму:
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:
public class Main {
static Integer visits;
public static void main(String[] args) {
visits++; // ✗
System.out.println(visits);
}
}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. - Упаковка в цикле создаёт новый объект почти на каждом круге, поэтому считают на примитивах.