Привіт! Сьогоднішня лекція буде досить...е-е-е...різносторонньою :) У тому сенсі, що ми розглянемо широкий спектр тем, але всі вони будуть стосуватися процесу створення об'єкта. Ми розберемо його від початку до кінця: як викликаються конструктори, як і в якому порядку ініціалізуються поля (включаючи статичні) тощо. Деякі моменти, розглянуті у статті, ми торкалися раніше, тому можеш переглянути матеріал про конструктор базових класів. Для початку давай згадаємо як відбувається створення об'єкта. Ну, як цей процес виглядає з точки зору розробника, ти добре пам'ятаєш: створив клас, написав new — і все готово :) Тут поговоримо про те, що відбувається всередині комп'ютера та Java-машини, коли ми пишемо, наприклад:

Cat cat = new Cat();
Ми раніше вже говорили про це, але на всяк випадок згадаємо:
  1. Спочатку для зберігання об'єкта виділяється пам'ять.
  2. Далі Java-машина створює посилання на цей об'єкт (у нашому випадку посилання — це Cat cat).
  3. На завершення відбувається ініціалізація змінних та виклик конструктора (цей процес ми розглянемо детальніше).
Крім того, з лекції про життєвий цикл об'єкта ти напевно пам'ятаєш, що він триває доти, доки на нього існує хоча б одне посилання. Якщо ж їх не залишилося, об'єкт стане здобиччю для збирача сміття. Послідовність дій при створенні об'єкта - 2Перші два пункти особливих запитань викликати не повинні. Виділення пам'яті — процес нескладний, і результат може бути тільки один із двох: або пам'ять є, або її немає :) Створення посилання — теж нічого незвичайного. А ось пункт номер три являє собою цілий набір операцій, які виконуються у строго визначеному порядку. Я не фанат зубріння як способу щось вивчити, але от у цьому процесі тобі варто добре розібратися, і знати цей порядок потрібно напам'ять. Коли ми говорили про процес створення об'єктів на попередніх лекціях, ти ще нічого толком не знав про спадкування, тому пояснити деякі моменти було проблематично. Зараз же обсяг твоїх знань достатньо великий, і ми, нарешті, можемо розглянути це питання повноцінно :) Отже, третій пункт каже що “на завершення відбувається ініціалізація змінних та виклик конструктора.” Але в якому порядку все це відбувається? Для кращого розуміння давай створимо два найпростіших класи — батьківський і клас-нащадок:

public class Car {

   public static int carCounter = 0;

   private String description = "Абстрактна машина";

   public Car() {
   }

   public String getDescription() {
       return description;
   }
}

public class Truck extends Car {

   private static int truckCounter = 0;

   private int yearOfManufacture;
   private String model;
   private int maxSpeed;

   public Truck(int yearOfManufacture, String model, int maxSpeed) {
       this.yearOfManufacture = yearOfManufacture;
       this.model = model;
       this.maxSpeed = maxSpeed;

       Car.carCounter++;
       truckCounter++;
   }
}
Клас Truck представляє собою реалізацію вантажівки: з полями, які відображають її рік випуску, модель і максимальну швидкість. Отже, ми хочемо створити один такий об'єкт:

public class Main {

   public static void main(String[] args) throws IOException {

       Truck truck = new Truck(2017, "Scania S 500 4x2", 220);
   }
}
Ось як буде виглядати цей процес з точки зору Java-машини:
  1. Перше, що відбудеться — проініціалізуються статичні змінні класу Car. Так-так, саме класу Car, а не Truck. Статичні змінні ініціалізуються ще до виклику конструкторів, і починається це у класі-батьку. Давай спробуємо перевірити. Виставимо лічильник carCounter у класі Car на 10 і спробуємо вивести його у консоль в обох конструкторах — Car і Truck.

    
    public class Car {
    
       public static int carCounter = 10;
    
       private String description = "Абстрактна машина";
    
       public Car() {
           System.out.println(carCounter);
       }
    
       public String getDescription() {
           return description;
       }
    }
    
    public class Truck extends Car {
    
       private static int truckCount = 0;
    
       private int yearOfManufacture;
       private String model;
       private int maxSpeed;
    
       public Truck(int yearOfManufacture, String model, int maxSpeed) {
           System.out.println(carCounter);
           this.yearOfManufacture = yearOfManufacture;
           this.model = model;
           this.maxSpeed = maxSpeed;
    
           Car.carCounter++;
           truckCount++;
       }
    }
    

    Ми спеціально поставили вивід у консоль на самому початку конструктора Truck, щоб точно знати: поля вантажівки на момент виводу carCounter у консоль ще не були ініціалізовані.

    А ось і результат:

    
    10
    10
    
  2. Після ініціалізації статичних змінних класу-батька ініціалізуються статичні змінні класу-нащадка. Тобто у нашому випадку — поле truckCounter класу Truck.

    Знову ж таки, проведемо експеримент і спробуємо вивести значення truckCounter всередині конструктора Truck до ініціалізації інших полів:

    
    public class Truck extends Car {
    
       private static int truckCounter = 10;
    
       private int yearOfManufacture;
       private String model;
       private int maxSpeed;
    
       public Truck(int yearOfManufacture, String model, int maxSpeed) {
           System.out.println(truckCounter);
           this.yearOfManufacture = yearOfManufacture;
           this.model = model;
           this.maxSpeed = maxSpeed;
    
           Car.carCounter++;
           truckCounter++;
       }
    }
    

    Як бачиш, значення 10 вже було присвоєне нашій статичній змінній у момент, коли конструктор Truck розпочав свою роботу.

  3. Час конструкторів ще не настав! Ініціалізація змінних триває. Третіми за чергою будуть ініціалізовані нестатичні змінні класу-батька. Як бачиш, наслідування суттєво ускладнює процес створення об'єкта, але тут нічого не вдієш: деякі речі в програмуванні доведеться просто запам'ятати :)

    Для експерименту ми можемо присвоїти змінній description класу Car якесь початкове значення, а потім змінити його у конструкторі.

    
    public class Car {
    
       public static int carCounter = 10;
    
       private String description = "Початкове значення поля description";
    
       public Car() {
           System.out.println(description);
           description = "Абстрактна машина";
           System.out.println(description);
       }
    
       public String getDescription() {
           return description;
       }
    }
    

    Запустимо наш метод main() зі створенням вантажівки:

    
    public class Main {
    
       public static void main(String[] args) throws IOException {
    
           Truck truck = new Truck(2017, "Scania S 500 4x2", 220);
       }
    }
    

    І отримаємо результат:

    
    Початкове значення поля description
    Абстрактна машина
    

    Це доводить, що на момент початку роботи конструктора Car у поля description вже було присвоєне значення.

  4. Нарешті, справа дійшла до конструкторів! Точніше, до конструктора базового класу. Початок його роботи — четвертий пункт у процесі створення об'єкта.

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

    
    public Car() {
       System.out.println("Привіт з конструктора Car!");
    }
    
    public Truck(int yearOfManufacture, String model, int maxSpeed) {
    
       System.out.println("Привіт з конструктора Truck!");
       this.yearOfManufacture = yearOfManufacture;
       this.model = model;
       this.maxSpeed = maxSpeed;
    
       Car.carCounter++;
       truckCounter++;
    }
    

    Запускаємо наш метод main() і дивимося на результат:

    
    Привіт з конструктора Car!
    Привіт з конструктора Truck!
    

    Чудово, значить ми не помилилися :) Їдемо далі.

  5. Тепер черга ініціалізації нестатичних полів класу-нащадка, тобто нашого класу Truck. Поля класу, об'єкт якого ми створюємо, ініціалізуються тільки у п'яту чергу! Дивовижно, але факт :) Знову ж таки, проведемо просту перевірку, таку ж, як і з батьківським класом: присвоїмо змінній maxSpeed якесь початкове значення і перевіримо у конструкторі Truck, що воно було присвоєне раніше, ніж конструктор почав роботу:

    
    public class Truck extends Car {
    
       private static int truckCounter = 10;
    
       private int yearOfManufacture;
       private String model;
       private int maxSpeed = 150;
    
       public Truck(int yearOfManufacture, String model, int maxSpeed) {
    
           System.out.println("Початкове значення maxSpeed = " + this.maxSpeed);
           this.yearOfManufacture = yearOfManufacture;
           this.model = model;
           this.maxSpeed = maxSpeed;
    
           Car.carCounter++;
           truckCounter++;
       }
    }
    

    Вивід у консоль:

    
    Початкове значення maxSpeed = 150
    

    Як бачиш, на момент старту конструктора Truck значення maxSpeed вже було рівне 150!

  6. Викликається конструктор дочірнього класу Truck.

    І тільки зараз, в останню чергу, буде викликаний конструктор того класу, об'єкт якого ми створюємо!

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

    Як бачиш, «конструювання» вантажівки, тобто процес створення об'єкта — штука непроста. Але ми, здається, розібрали його до найменших деталей :)

Послідовність дій при створенні об'єкта - 3Чому так важливо добре розуміти цей процес? Уяви, наскільки несподіваними можуть виявитися результати створення звичайного об'єкта, якщо не знати точно, що відбувається «під капотом» :) Саме час повернутися до курсу і розв'язати кілька задач! Удачі, і до нових зустрічей :)