Давай розберемося в черговості виконання коду в блоках ініціалізації (статичних та нестатичних), конструкторах, а також ініціалізації статичних і нестатичних полів. Розбиратимемося на практиці, виконуючи код.

На вході у нас є клас із повним набором різноманітних елементів:

public class MyClass {
    static {
        System.out.println("Статичний блок №1.");
    }

    public static String staticField = setStaticField();

    public MyClass() {
        System.out.println("Конструктор.");
    }

    static {
        System.out.println("Статичний блок №2.");
    }

    {
        System.out.println("Блок ініціалізації №1.");
    }

    public String nonStaticField = setNonStaticField();

    {
        System.out.println("Блок ініціалізації №2.");
    }

    private String setNonStaticField() {
        System.out.println("Нестатичне поле.");
        return "nonStaticField";
    }

    private static String setStaticField() {
        System.out.println("Статичне поле.");
        return "staticField";
    }

    public static void print() {
        System.out.println("Метод print.");
    }
}

Тепер поруч із цим класом створимо ще один, у ньому метод main і запустимо його:

public class Solution {
    public static void main(String args[]) {
        System.out.println("hello");
    }
}
У виведенні немає нічого з класу MyClass. Оскільки до MyClass не було звернень, клас взагалі не був завантажений. Спробуємо тепер викликати у класу MyClass статичний метод print(). Двічі.
public class Solution {
    public static void main(String args[]) {
        MyClass.print();
        MyClass.print();
    }
}

Виведення:

Статичний блок №1.
Статичне поле.
Статичний блок №2.
Метод print.
Метод print.

Виконалися лише статичні блоки ініціалізації та ініціалізувалося статичне поле. Причому ше один раз. Справа в тому, що під час другого виклику методу print() клас вже був завантажений. Запам’ятовуємо: статичні поля та блоки ініціалізації виконуються один раз під час першої взаємодії з класом.

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

Далі замість виклику статичного методу спробуємо створити два об’єкти нашого класу:

public class Solution {
    public static void main(String args[]) {
        new MyClass();
        System.out.println();
        new MyClass();
    }
}

Виведення:

Статичний блок №1.
Статичне поле.
Статичний блок №2.
Блок ініціалізації №1.
Нестатичне поле.
Блок ініціалізації №2.
Конструктор.

Блок ініціалізації №1.
Нестатичне поле.
Блок ініціалізації №2.
Конструктор.

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

Ускладнимо приклад і візьмемо два класи, причому один з них успадковує інший:

public class ParentClass {
    static {
        System.out.println("Статичний блок №1 батьківського класу.");
    }

    public static String parentStatic = setParentStatic();

    static {
        System.out.println("Статичний блок №2 батьківського класу.");
    }

    {
        System.out.println("Блок ініціалізації №1 батьківського класу.");
    }

    public String parentNonStatic = setParentNonStatic();

    {
        System.out.println("Блок ініціалізації №2 батьківського класу.");
    }

    public ParentClass() {
        System.out.println("Конструктор батьківського класу.");
    }

    private String setParentNonStatic() {
        System.out.println("Нестатичне поле батьківського класу.");
        return "parentNonStatic";
    }

    private static String setParentStatic() {
        System.out.println("Статичне поле батьківського класу.");
        return "parentStatic";
    }

    public String setChildNonStatic1() {
        System.out.println("Нестатичне поле дочірнього класу №1.");
        return "childNonStatic2" + parentNonStatic;
    }
}

public class ChildClass extends ParentClass {
    static {
        System.out.println("Статичний блок №1 дочірнього класу.");
    }

    public static String childStatic = setChildStatic();

    static {
        System.out.println("Статичний блок №2 дочірнього класу.");
    }

    public String childNonStatic1 = setChildNonStatic1();

    {
        System.out.println("Блок ініціалізації №1 дочірнього класу.");
    }

    public String childNonStatic2 = setChildNonStatic2();

    {
        System.out.println("Блок ініціалізації №2 дочірнього класу.");
    }

    public ChildClass() {
        System.out.println("Конструктор дочірнього класу.");
    }

    private String setChildNonStatic2() {
        System.out.println("Нестатичне поле дочірнього класу №2.");
        return "childNonStatic";
    }

    private static String setChildStatic() {
        System.out.println("Статичне поле дочірнього класу.");
        return "childStatic";
    }
}

Створимо два об’єкти дочірнього класу:

public class Solution {
    public static void main(String[] args) {
        new ChildClass();
        System.out.println();
        new ChildClass();
    }
}

Виведення:

Статичний блок №1 батьківського класу.
Статичне поле батьківського класу.
Статичний блок №2 батьківського класу.
Статичний блок №1 дочірнього класу.
Статичне поле дочірнього класу.
Статичний блок №2 дочірнього класу.
Блок ініціалізації №1 батьківського класу.
Нестатичне поле батьківського класу.
Блок ініціалізації №2 батьківського класу.
Конструктор батьківського класу.
Нестатичне поле дочірнього класу №1.
Блок ініціалізації №1 дочірнього класу.
Нестатичне поле дочірнього класу №2.
Блок ініціалізації №2 дочірнього класу.
Конструктор дочірнього класу.

Блок ініціалізації №1 батьківського класу.
Нестатичне поле батьківського класу.
Блок ініціалізації №2 батьківського класу.
Конструктор батьківського класу.
Нестатичне поле дочірнього класу №1.
Блок ініціалізації №1 дочірнього класу.
Нестатичне поле дочірнього класу №2.
Блок ініціалізації №2 дочірнього класу.
Конструктор дочірнього класу.

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

На практиці ти можеш і не зустріти класів, що містять відразу всі перелічені елементи, але пам’ятати, що за чим ініціалізується буде корисним. А ще про це часто запитують на співбесідах 😊