JavaRush /Java блог /Random /Автоупаковка и распаковка в Java
Viacheslav
3 уровень

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

Статья из группы Random
<h2>Вступление</h2>Язык программирования, как и язык на котором говорят люди, живёт и меняется, в нём появляются новые явления, чтобы языком было пользоваться удобнее. А как мы знаем язык должен удобно выражать наши мысли.
Автоупаковка и распаковка в Java - 1
Итак, в 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 файл для вашего класса. Мы увидим содержимое вроде такого:
Автоупаковка и распаковка в Java - 2
Этот тот самый пресловутый «байткод». Но важно нам сейчас то, что мы видим. Сначала в стэк выполнения метода помещается примитив 8080, а дальше выполняется Integer.valueOf. Это и есть та «магия» boxing. А внутри магия выглядит вот так:
Автоупаковка и распаковка в Java - 3
То есть по сути будет взят новый 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 - 4
Как видно, никакой магии. Всё в рамках Java. Просто оно работает «само». Для нашего удобства. <h2>Грабли</h2>
Автоупаковка и распаковка в Java - 5
Любой инструмент при неправильном использовании становится грозным оружием против самого себя. И механизм автоупаковка и распаковка 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
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Сергей Уровень 32
17 мая 2020
HTML теги поправьте на странице