JavaRush /Java блог /Random UA /Автоупаковка та розпакування в Java
Viacheslav
3 рівень

Автоупаковка та розпакування в Java

Стаття з групи Random UA
<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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ