1. Ініціалізація змінних

Як вам уже відомо, ви можете у своєму класі оголосити кілька змінних класу, і не просто оголосити, а відразу ініціалізувати їх початковими значеннями.

Натомість ці самі змінні можна ініціалізувати і в конструкторі. Тому теоретично можлива ситуація, коли одним і тим самим змінним класу присвоюються значення двічі. Приклад

Код Примітка
class Cat
{
   public String name;
   public int age = -1;

   public Cat(String name, int age)
   {
     this.name = name;
     this.age = age;
   }

   public Cat()
   {
     this.name = "Безіменний";
   }
}



Змінній age присвоюється початкове значення




Початкове значення затирається


Для age використовується початкове значення
 Cat cat = new Cat("Мурчик", 2);
Так можна: буде викликано перший конструктор
 Cat cat = new Cat();
Так можна: буде викликано другий конструктор

От що відбуватиметься під час виконання коду Cat cat = new Cat("Мурчик", 2);:

  • Створюється об'єкт типу Cat.
  • Ініціалізуються всі змінні класу своїми початковими значеннями.
  • Викликається конструктор і виконується його код.

Тобто змінні класу спочатку ініціалізуються своїми значеннями, а вже потім виконується код конструкторів.


2. Порядок ініціалізації змінних класу

Змінні не просто ініціалізуються до початку роботи конструктора: вони ще й ініціалізуються в чітко визначеному порядку — порядку їх оголошення в класі.

Розгляньмо отакий цікавий код:

Код Примітка
public class Solution
{
   public int a = b + c + 1;
   public int b = a + c + 2;
   public int c = a + b + 3;
}

Такий код не скомпілюється, оскільки на момент створення змінної а змінних b і c ще не існує. А отак записати можна, і цей код чудово скомпілюється і працюватиме.

Код Примітка
public class Solution
{
   public int a;
   public int b = a + 2;
   public int c = a + b + 3;
}


0
0+2
0+2+3

Примітка. Сподіваюсь, ви пам'ятаєте, що ваш код має бути прозорим для інших розробників, отож такі прийоми краще не використовувати — це погіршує читомість коду.

Не забувайте, що всі змінні класу, перш ніж їм присвоять певні значення, мають значення за замовчуванням. Для типу int це нуль.

Коли віртуальна машина Java ініціалізуватиме змінну а, вона просто присвоїть їй значення за замовчуванням для типу int — 0.

Коли черга дійде до b, змінна a вже буде відома й міститиме значення, тому JVM присвоїть змінній b значення 2.

Ну а коли дійдеться до змінної c, змінні а і b вже будуть проініціалізовані, і JVM легко обчислить початкове значення для с: 0+2+3.

Якщо ви створили змінну всередині методу, ви не можете її використовувати, не присвоївши їй перед цим певне значення. А зі змінними класу це не так. Якщо змінній класу не присвоєно початкове значення, значить, їй присвоюється значення за замовчуванням.


3. Константи

Коли ми вже продовжуємо розбиратися в процесі створення об'єкта, варто торкнутися питання ініціалізації констант — змінних класу, які мають модифікатор final.

Якщо змінна класу має модифікатор final, їй має бути присвоєно початкове значення. Це ви вже знаєте, і тут немає нічого дивного.

А от чого ви не знаєте: початкове значення можна не присвоювати відразу, якщо присвоїти його в конструкторі. І це чудово працюватиме для final-змінної. Єдина вимога — якщо конструкторів декілька, то final-змінній має бути присвоєно значення у всіх конструкторах.

Приклад:

public class Cat
{
   public final int maxAge = 25;
   public final int maxWeight;

   public Cat (int weight)
   {
      this.maxWeight = weight; // внесення початкового значення в константу
   }
}


4. Код у конструкторі

І ще кілька важливих зауважень щодо конструкторів. Згодом у процесі вивчення Java ви зіткнетеся з такими речами як успадкування, серіалізація, винятки тощо. Усі вони по-різному впливають на роботу конструкторів. Наразі немає сенсу дуже заглиблюватися в ці теми, але принаймні зачепити їх ми мусимо.

От, наприклад, одне з вагомих зауважень. Теоретично в конструкторі можна писати код будь-якої складності. Однак не треба цього робити. Приклад:

class FilePrinter
{
   public String content;

   public FilePrinter(String filename) throws Exception
   {
      FileInputStream input = new FileInputStream(filename);
      byte[] buffer = input.readAllBytes();
      this.content = new String(buffer);
   }

   public void printFile()
   {
      System.out.println(content);
   }
}






Відкриваємо потік читання файлу
Зчитуємо файл у масив байтів
Зберігаємо масив байтів у вигляді рядка




Виводимо вміст файлу на екран

У конструкторі класу FilePrinter ми одразу відкрили байтовий потік до файлу і прочитали його вміст. Це досить складна поведінка, яка потенційно може спричинити помилки.

А якби такого файлу не було? А якби були проблеми з його читанням? А якби він був занадто великим?

Складна логіка передбачає велику ймовірність помилок і код, який має правильно обробляти винятки.

Приклад 1 — Серіалізація

У стандартній Java-програмі є багато ситуацій, коли об'єкти вашого класу створюєте не ви. Наприклад, ви хочете передати об'єкт мережею: у такому разі Java-машина сама перетворить ваш об'єкт на набір байтів, передасть його та знову за набором байтів створить об'єкт.

І отут виявиться, що на іншому комп'ютері немає вашого файлу, в конструкторі виникне помилка, і ніхто її не обробить — усе це разом може призвести до закриття програми.

Приклад 2 — Ініціалізація полів класу

Якщо конструктор вашого класу може кинути checked-виняток (містить ключове слово throws), ви повинні перехопити цей виняток у методі, який створює ваш об'єкт.

А якщо такого методу немає? Приклад:

Код  Примітка
class Solution
{
   public FilePrinter reader = new FilePrinter("c:\\readme.txt");
}
Такий код не скомпілюється.

Конструктор класу FilePrinter містить checked-винятки: ви не можете створити об'єкт FilePrinter, не обгорнувши його конструкцією try-catch. А try-catch можна писати тільки в методі.



5. Конструктор базового класу

У попередніх лекціях ми трохи обговорювали успадкування. На жаль, докладно обговорювати успадкування та ООП ми будемо на рівні, присвяченому ООП, а конструкторів це стосується вже зараз.

Якщо ви успадкуєте свій клас від іншого класу, фактично в об'єкт вашого класу буде вбудовано об'єкт батьківського класу, причому цей батьківський клас має свої змінні класу та свої конструктори.

Тому вам дуже важливо знати й розуміти, як відбуваються ініціалізація параметрів і виклик конструкторів, коли ваш клас має батьківський клас, чиї змінні та методи ви успадковуєте.

Класи

Як же нам дізнатися, в якому порядку ініціалізуються змінні й викликаються конструктори? Для початку напишімо код двох класів, один з яких успадковується від іншого:

Код Примітка
class ParentClass
{
   public String a;
   public String b;

   public ParentClass()
   {
   }
}

class ChildClass extends ParentClass
{
   public String c;
   public String d;

   public ChildClass()
   {
   }
}










Клас ChildClass успадковується від класу ParentClass.

Нам треба визначити, в якому порядку ініціалізуються змінні й викликаються конструктори. Зробити це допоможе логування.

Логування

Логуванням називається процес запису в консоль або у файл тих дій, які відбуваються під час роботи програми.

Визначити, що відбувся виклик конструктора, досить просто: потрібно в тілі конструктора вивести в консоль повідомлення про це. А от як визначити, що змінну ініціалізовано?

Насправді це теж не дуже складно: потрібно написати спеціальний метод, котрий повертатиме значення, яким ініціалізується змінна класу, і залогувати цей факт. Цей код може мати, приміром, такий вигляд:

Остаточний код

public class Main
{
   public static void main(String[] args)
   {
      ChildClass obj = new ChildClass();
   }

   public static String print(String text)
   {
      System.out.println(text);
      return text;
   }
}

class ParentClass
{
   public String a = Main.print("ParentClass.a");
   public String b = Main.print("ParentClass.b");

   public ParentClass()
   {
      Main.print("ParentClass.constructor");
   }
}

class ChildClass extends ParentClass
{
   public String c = Main.print("ChildClass.c");
   public String d = Main.print("ChildClass.d");

   public ChildClass()
   {
      Main.print("ChildClass.constructor");
   }
}




Створюємо об'єкт типу ChildClass


Цей метод записує в консоль переданий текст і повертає його





Оголошуємо ParentClass

Пишемо текст і ним же ініціалізуємо змінні




Записуємо в консоль повідомлення про виклик конструктора. Значення, що повертається, ігноруємо.


Оголошуємо ChildClass

Пишемо текст і ним же ініціалізуємо змінні




Записуємо в консоль повідомлення про виклик конструктора. Значення, що повертається, ігноруємо.

Якщо виконати цей код, на екран буде виведено текст:

Виведення на екран методу Main.print()
ParentClass.a
ParentClass.b
ParentClass.constructor
ChildClass.c
ChildClass.d
ChildClass.constructor

Отже, ви завжди можете особисто переконатися, що змінні класу ініціалізуються до виклику його конструктора. Уся ініціалізація членів базового класу відбувається до ініціалізації членів класу-спадкоємця.