<h2>Вступление</h2>Язык программирования, как и язык на котором говорят люди, живёт и меняется, в нём появляются новые явления, чтобы языком было пользоваться удобнее. А как мы знаем язык должен удобно выражать наши мысли.
Итак, в Java SE 5 был представлен механизм упаковки/распаковки (boxing/unboxing). И особенности этого средства выражений мыслей посвящён отдельный tutorial от Oracle: Autoboxing and Unboxing.
<h2>Автоупаковка Boxing</h2>Давайте, рассмотрим пример автоупаковки Boxing.
Сначала, посмотрим, как оно работает. Воспользуемся сайтом compilejava.net и создадим класс:
public class App {
public static void main(String[] args) {
Integer portNumber = 8080;
if (args.length != 0) {
portNumber = Integer.valueOf(args[0]);
}
System.out.println("Port number is: " + portNumber);
}
}
Простенький код. Можем указать входной параметр и поменять значение порта.
Как мы видим, т.к. мы считываем значение порта из String
параметров, мы получаем Integer
путём получения его через Integer.valueOf
. Поэтому, мы вынуждены указывать его не примитивным типом, а как объектный тип Integer
.
И тут получаем с одной стороны, у нас переменная объектная, а значение по умолчанию – примитив. И оно работает. Но мы ведь не верим в магию?
Давайте заглянем «под капот», что называется.
Скачаем с compilejava.net исходный код, нажав «Download ZIP». После этого извлечём скачанный архив в каталог и перейдём в него.
Теперь выполним: javap -c -p App.class
где App.class - скомпилированный class файл для вашего класса.
Мы увидим содержимое вроде такого:
Этот тот самый пресловутый «байткод». Но важно нам сейчас то, что мы видим. Сначала в стэк выполнения метода помещается примитив 8080, а дальше выполняется Integer.valueOf. Это и есть та «магия» boxing.
А внутри магия выглядит вот так:
То есть по сути будет взят новый
Integer
или будет получен Integer
из кэша (кэш ничто иное как просто массив из Integer) в зависимости от величины значения числа.
Естественно, Integer
не одному так повезло. Есть целый список соотносящихся примитивных типов и их обёрток (классов, представляющие примитивы в мире ООП). Данный список приведён в самом низу Tutorial от Oracle: «Autoboxing and Unboxing».
Стоит сразу отметить, что массивы из примитивов не имеют «обёртки» без подключения каких-либо сторонних библиотек. Т.е. Arrays.asList
не сделает из int[]
для нас List
из Integer
’ов.
<h2>Распаковка Unboxing</h2>Обратный процесс к boxing называется unboxing распаковка.
Рассмотрим пример распаковки:
public class App {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Please, enter params");
return;
}
int value = Math.abs(Integer.valueOf(args[0]));
System.out.println("Absolute value is: " + value);
}
}
Math.abs
принимает только примитивы. Что же делать? У класса обёртки есть на этот случай специальный метод, возвращающий примитив. Например, для Integer
это метод intValue.
Если мы посмотрим в байткод, то так и есть:
Как видно, никакой магии. Всё в рамках Java. Просто оно работает «само». Для нашего удобства.
<h2>Грабли</h2>
Любой инструмент при неправильном использовании становится грозным оружием против самого себя. И механизм автоупаковка и распаковка boxing/unboxing в Java не исключение.
Первое, очевидное, сравнение через
==
. Думаю, это понятно, но разберём ещё раз:
public static void main(String[] args) {
Integer inCacheValue = 127;
Integer inCacheValue2 = 127;
Integer notInCache = 128; // new Integer(129)
Integer notInCache2 = 128; // new Integer(129)
System.out.println(inCacheValue == inCacheValue2); //true
System.out.println(notInCache == notInCache2); //false
}
В первом случае, значение берётся из кэша Integer
значений (см. объяснение Boxing выше), а во втором случае будет создаваться каждый раз новый объект. Но тут стоит оговориться. Это поведение зависит от верхней границы кэша (java.lang.Integer.IntegerCache.high). Кроме того, эта граница может измениться и из-за других настроек. На эту тему можно ознакомиться с обсуждением на stackoverflow: How large is the Integer cache?
Естественно, объекты нужно сравнивать через equals:
System.out.println(notInCache.equals(notInCache2));
Вторая проблема, связанная с этим же механизмом – производительность. Любая упаковка в Java равносильна созданию нового объекта. Если число не входит в значения из кэша (т.е. в -128 до 127), то будет каждый раз создаваться новый объект. Если вдруг упаковка (т.е. boxing) будет производиться в цикле, это вызовет огромный прирост ненужных объектов и потребление ресурсов на работу сборщика мусора. Поэтому, не стоит слишком безрассудно относится к этому.
Третьи не менее больные грабли вытекают из того же механизма:
public static void check(Integer value) {
if (value <= 0) {
throw new IllegalStateException("Value is too small");
}
}
В этом коде человек явно пытался не пройти мимо ошибки. Но тут нет проверки на null
. Если на вход придёт null
, то вместо понятной ошибки мы получим невнятный NullPointerException
. Потому что для сравнения Java попробует выполнить value.intValue
и свалится, т.к. value
будет null
.
<h2>Вывод</h2>Механизм boxing/unboxing позволяет программисту писать меньше кода и даже порой не думать о преобразовании из примитивов в объекты и обратно. Но это не значит, что нужно забывать, как это работает. Иначе можно допустить ошибку, которая может всплыть не сразу. Не стоит полагаться на части системы, которые не полностью под нашим контролем (как например граница integer). Но и не стоит забывать про все преимущества классов-обёрток (вроде Integer
). Часто эти классы-обёртки имеют набор дополнительных статических методов, которые сделают вашу жизнь лучше, а код – выразительнее. Вот пример в догонку:
public static void main(String[] args) {
int first = 1;
int second = 5;
System.out.println(Integer.max(first, second));
System.out.println(Character.toLowerCase('S'));
}
Так же правильный вывод из всего - нет магии, есть какая-то реализация. И не везде будет всегда то, что мы ожидаем. Например тут нет упаковки:
System.out.println("The number is " + 8);
Пример выше компилятором будет соптимизирован в одну строку. То есть словно вы написали «The number is 8».
И в примере ниже тоже не будет упаковки:
public static void main(String[] args) {
System.out.println("The number is " + Math.abs(-2));
}
Как же так, когда у нас println
принимает на вход объект и нужно как-то соединить строки. Строки… да, именно поэтому нет упаковки, как таковой. У Integer
есть статические методы, но некоторые из них уровня package
. То есть мы их не можем использовать, а вот в самой Java они могут активно использоваться. Вот тут как раз такой случай. Будет вызван метод getChars, который из числа делает массив символов. Опять же, никакой магии, только Java ).
Так что в любой непонятной ситуации стоит просто посмотреть реализацию и хоть что-то да встанет на свои места.
#Viacheslav
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ