Вступ
Основне призначення комп'ютерних програм – обробка даних. Щоб обробити дані, потрібно їх якось зберігати. Пропоную розібратися з тим, як відбувається зберігання даних.![Привласнення та ініціалізація в Java - 1](https://cdn.javarush.com/images/article/bcefb1cb-a4fc-47e0-9298-f3620e515d4e/512.jpeg)
Змінні
Змінні (variables) – це контейнери, які зберігають будь-які дані. Подивимося офіційний Tutorial від Oracle: Declaring Member Variables . Згідно з цим Tutorial, існує кілька типів змінних:- Поля (fields): змінні, оголошені у класі;
- Локальні змінні (local variables): змінні в методі або в блоці коду;
- Параметри ( parameters ): Змінні в оголошенні методу (у сигнатурі).
- Тип змінної показує, які дані представляє ця змінна (тобто які дані може зберігати). Як знаємо, тип змінної може бути примітивним (primitives primitives ) чи об'єктним , не примітивними (Non-primitive). При об'єктних змінних їх тип описується певним класом.
- Назва змінної мусить бути з невеликої літери, у camel case. Докладніше про назву можна прочитати в " Variables: Naming ".
Оголошення змінної (Declaration)
Тож ми згадали, що таке змінна. Щоб зі змінною почати працювати потрібно її оголосити. Для початку розберемося з локальною змінною. Замість IDE для зручності скористаємося онлайн рішенням від tutorialspoint: Online IDE . Виконаємо в них online IDE ось таку просту програмку:public class HelloWorld{
public static void main(String []args){
int number;
System.out.println(number);
}
}
Отже, очевидно, ми оголосабо локальну змінну з ім'ям number
і типом int
. Натискаємо кнопку «Execute» та отримуємо помилку:
HelloWorld.java:5: error: variable number might not have been initialized
System.out.println(number);
Що сталося? Ми оголосабо змінну, але її не ініціалізували. Варто зауважити, що ця помилка відбулася не в момент виконання (тобто не в Runtime), а в момент компіляції. Розумний компілятор перевірив, чи локальна змінна буде ініціалізована до звернення до неї чи ні. Тому з цього випливають такі твердження:
- Звернення до локальних змінних має бути виконане лише після того, як вони будуть ініціалізовані;
- Локальні змінні немає значень за умовчанням;
- Перевірка значень локальних змінних виконується на момент компіляції.
Ініціалізація локальної змінної
Ініціалізація змінних одна з найбільш складних тем у Java, т.к. дуже тісно пов'язана з роботою з пам'яттю, з реалізацією JVM, специфікацією JVM та іншими не менш страшними та хитрими речами. Але можна спробувати розібратися хоч якоюсь мірою. Ходімо від простого до складного. Щоб ініціалізувати змінну скористаємося оператором присвоєння та змінимо рядок у нашому минулому коді:int number = 2;
У такому варіанті помилок не буде і на екран виведеться значення. Що ж відбувається у цьому випадку? Давайте спробуємо поміркувати. Якщо ми хочемо надати змінній якесь значення, то ми хочемо, щоб ця змінна зберігала значення. Виходить, що значення має десь зберігатися, але де? На диску? Але це дуже повільно та може на нас накладати обмеження. Виходить, єдине, де ми можемо швидко та ефективно зберігати дані "тут і зараз" це пам'ять. Отже, нам потрібно виділити у пам'яті якесь місце. Так і є. При ініціалізації змінної під неї буде виділено місце в пам'яті, відведеній java процесу, в рамках якого виконуватиметься наша програма. Пам'ять, що виділяється java процесу, розділена на кілька областей або зон. У якій їх буде виділено місце залежить від цього, якого типу було оголошено змінна. Пам'ять поділяється на такі розділи: Heap, Stack і Non-Heap . Почнемо зі стекової пам'яті. Stackперекладається як стопка (наприклад, стопка книг). Є LIFO структурою даних (Last In, First Out). Тобто як стопка книг. Коли ми додаємо до неї книги – ми кладемо їх зверху, а коли забираємо – беремо верхню (тобто ту, яка додана останньою). Отже, ми запускаємо нашу програму. Як знаємо, Java програму виконує JVM, тобто віртуальна Java машина. JVM повинна знати те, звідки має розпочатися виконання програми. Для цього ми оголошуємо main метод, який називається "точкою входу". Для виконання JVM створюється основний потік (Thread). Під час створення потоку йому виділяється свій стек у пам'яті. Цей стек складається з фреймів. При виконанні кожного нового методу в потоці під нього буде виділено новий фрейм і доданий на вершину стеку (як нова книжка у стопці книг). Цей кадр буде містить посилання на об'єкти та примітивні типи. Так, наш int буде зберігатися в стеку, т.к. int це примітивний тип. Перш ніж виділити фрейм JVM має розуміти, що туди зберігати. Саме тому ми отримаємо помилку «variable might not have been initialized», адже якщо вона не ініціалізована, то JVM не зможе нам підготувати стек. Тому при компіляції програми розумний компілятор допоможе нам не припуститися помилки і не зламати все. (!) Для наочності раджу супер-пупер статтю: " Java Stack and Heap: Java Memory Allocation Tutorial ". У ній посилаються на не менш круте відео:
Після завершення виконання методу зі стека потоку видалятимуться кадри, виділені під ці методи, а разом з ними і очищатимуться пам'ять, виділена під цей кадр з усіма даними.
Ініціалізація локальних об'єктних змінних
Давайте знову змінимо наш код трохи хитріший:public class HelloWorld{
private int number = 2;
public static void main(String []args){
HelloWorld object = new HelloWorld();
System.out.println(object.number);
}
}
Що ж тут відбуватиметься? Давайте ще раз міркувати. JVM дізнається у тому, звідки їй виконувати програму, тобто. вона бачить головний метод. Вона створює потік, під нього виділяє пам'ять (адже потоку треба десь зберігати дані, які потрібні для виконання). У цьому вся потоці виділяється кадр під метод main. Далі ми створюємо об'єкт HelloWorld. Цей об'єкт вже створюється над стеку, а хіпі. Тому що об'єкт у нас не примітивний тип, а об'єктний. А в стеку зберігатиметься лише посилання на об'єкт у хіпі (адже ми якось повинні звертатися до цього об'єкта). Далі у стеку методу main будуть виділені фрейми до виконання методу println. Після виконання методу main будуть знищені всі кадри. При знищенні кадру будуть знищені всі дані. Об'єкт об'єкта не буде знищений відразу. Спочатку на нього буде знищено посилання і таким чином на об'єкт object більше ніхто посилатися не буде і доступу більше до цього об'єкта пам'яті не отримає. Розумна JVM має свій механізм для такого – збирач сміття (garbage collector або скорочено GC). Він і видаляє з пам'яті такі об'єкти, куди більше ніхто не посилається. Цей процес знову ж таки був описаний у посиланні, що було наведено вище. Там навіть відео є із поясненням.
Ініціалізація полів
Ініціалізація полів, зазначених у класі, відбувається особливим чином залежно від того, чи є поле статичним чи ні. Якщо поле має ключове слово static, то це поле відноситься до самого класу, а не слово static не вказано, то дане поле відноситься до екземпляра класу. Давайте розглянемо це з прикладу:public class HelloWorld{
private int number;
private static int count;
public static void main(String []args){
HelloWorld object = new HelloWorld();
System.out.println(object.number);
}
}
У цьому прикладі ініціалізація полів відбувається в різний час. Поле номера буде ініціалізовано після того, як буде створено об'єкт object класу HelloWorld. А ось поле count буде ініціалізоване тоді, коли клас буде завантажений віртуальною машиною Java. Завантаження класів – це окрема тема, тому сюди не будемо домішувати її. Просто варто знати, що статичні змінні ініціалізуються тоді, коли про клас стає відомо під час виконання. Тут важливіше інше, і Ви вже це помітабо. Ми ніде не вказали значення, а воно працює. І дійсно. Змінні, які є полями, якщо їм не вказано значення, вони ініціалізуються значенням за промовчанням. Для числових значенням це 0 або 0.0 для чисел із плаваючою точкою. Для boolean це false. А для всіх змінних типів об'єктів значення буде null (про це ми ще поговоримо). Здавалося б, чому так? А тому, що об'єкти створюються в Heap (у купі). Робота з цією областю виконується в Runtime. І ми в runtime можемо ініціалізувати ці змінні, на відміну від стеку, пам'ять під який має бути підготовлена ще до виконання. Так влаштована робота з пам'яттю Java. Але є тут ще одна особливість. У цьому маленькому шматочку торкаються різних куточків пам'яті. Як пам'ятаємо, в Stack пам'яті під метод main виділяється кадр. У цьому кадрі зберігається посилання (reference) на об'єкт у Heap пам'яті. Але де тоді зберігається Count? Як ми пам'ятаємо, ця змінна ініціалізується одразу, до створення об'єкта в хіпі. Ось тут справді хитре питання. До Java 8 існувала область пам'яті, яка називається PERMGEN. Починаючи з Java 8 ця область зазнала змін і називається METASPACE. Насправді, статичні змінні є частиною описи класу, тобто. його метаданими. Тому, логічно, що зберігається в сховищі метаданих, METASPACE. MetaSpace відноситься до тієї ж Non-Heap області пам'яті, є її частиною. Важливо ще враховувати те, що враховується порядок, у якому оголошено змінні. Наприклад, у цьому коді помилка:
public class HelloWorld{
private static int b = a;
private static int a = 1;
public static void main(String []args){
System.out.println(b);
}
}
Що таке null
Як було сказано вище, змінні об'єктних типів, якщо вони є полями класу, ініціалізуються значеннями за промовчанням і за промовчанням є null. Але що таке null в Java? Перше, що важливо пам'ятати – примітивні типи не можуть бути null. А все тому, що null - це особливе посилання (reference), яке не посилається нікуди, ні на який об'єкт. Тому, тільки об'єктна змінна може дорівнювати null. Друге, що важливо розуміти, що null це посилання, reference. Я reference теж мають свою вагу. На цю тему можна почитати питання на stackoverflow: " Does null variable require space in memory ".Блоки ініціалізації
Розглядаючи ініціалізацію змінних злочинів не розглянути блоки ініціалізації. Виглядає це так:public class HelloWorld{
static {
System.out.println("static block");
}
{
System.out.println("block");
}
public HelloWorld () {
System.out.println("Constructor");
}
public static void main(String []args){
HelloWorld obj = new HelloWorld();
}
}
Порядок виведення буде: static block, block, constructor. Як бачимо, блоки ініціалізації виконуються раніше, ніж конструктор. І іноді це може бути зручним засобом ініціалізації.