«1. Обзор

JVM использует два различных метода для инициализации экземпляров объектов и классов.

В этой быстрой статье мы увидим, как компилятор и среда выполнения используют методы \u003cinit\u003e и \u003cclinit\u003e для целей инициализации.

2. Методы инициализации экземпляра

Давайте начнем с простого выделения и назначения объекта:

Object obj = new Object();

Если мы скомпилируем этот фрагмент и посмотрим на его байт-код с помощью javap -c, мы увидим что-то вроде :

0: new           #2      // class java/lang/Object
3: dup
4: invokespecial #1      // Method java/lang/Object."<init>":()V
7: astore_1

Для инициализации объекта JVM вызывает специальный метод с именем \u003cinit\u003e. На жаргоне JVM этот метод является методом инициализации экземпляра. Метод является инициализацией экземпляра тогда и только тогда, когда:

    Он определен в классе Его имя \u003cinit\u003e Он возвращает void

Каждый класс может иметь ноль или более методов инициализации экземпляра. Эти методы обычно соответствуют конструкторам в языках программирования на основе JVM, таких как Java или Kotlin.

2.1. Конструкторы и блоки инициализации экземпляров

Чтобы лучше понять, как компилятор Java транслирует конструкторы в \u003cinit\u003e, рассмотрим другой пример:

public class Person {
    
    private String firstName = "Foo"; // <init>
    private String lastName = "Bar"; // <init>
    
    // <init>
    {
        System.out.println("Initializing...");
    }

    // <init>
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // <init>
    public Person() {
    }
}

Это байт-код для этого класса:

public Person(java.lang.String, java.lang.String);
  Code:
     0: aload_0
     1: invokespecial #1       // Method java/lang/Object."<init>":()V
     4: aload_0
     5: ldc           #7       // String Foo
     7: putfield      #9       // Field firstName:Ljava/lang/String;
    10: aload_0
    11: ldc           #15      // String Bar
    13: putfield      #17      // Field lastName:Ljava/lang/String;
    16: getstatic     #20      // Field java/lang/System.out:Ljava/io/PrintStream;
    19: ldc           #26      // String Initializing...
    21: invokevirtual #28      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    24: aload_0
    25: aload_1
    26: putfield      #9       // Field firstName:Ljava/lang/String;
    29: aload_0
    30: aload_2
    31: putfield      #17      // Field lastName:Ljava/lang/String;
    34: return

Несмотря на то, что конструктор и блоки инициализатора в Java разделены, они находятся в одном и том же методе инициализации экземпляра на уровне байт-кода. На самом деле, этот метод \u003cinit\u003e:

    Сначала инициализирует поля firstName и lastName (индексы с 0 по 13) Затем он выводит что-то на консоль как часть блока инициализатора экземпляра (индексы с 16 по 21) И, наконец, он обновляет переменные экземпляра аргументами конструктора

Если мы создадим Person следующим образом:

Person person = new Person("Brian", "Goetz");

Тогда это преобразуется в следующий байт-код:

0: new           #7        // class Person
3: dup
4: ldc           #9        // String Brian
6: ldc           #11       // String Goetz
8: invokespecial #13       // Method Person."<init>":(Ljava/lang/String;Ljava/lang/String;)V
11: astore_1

На этот раз JVM вызывает другой \u003cinit \u003e метод с сигнатурой, соответствующей конструктору Java.

Ключевым выводом здесь является то, что конструкторы и другие инициализаторы экземпляров эквивалентны методу \u003cinit\u003e в мире JVM.

3. Методы инициализации класса

В Java блоки статического инициализатора полезны, когда мы собираемся инициализировать что-то на уровне класса:

public class Person {

    private static final Logger LOGGER = LoggerFactory.getLogger(Person.class); // <clinit>

    // <clinit>
    static {
        System.out.println("Static Initializing...");
    }

    // omitted
}

Когда мы компилируем предыдущий код, компилятор транслирует статический block в метод инициализации класса на уровне байт-кода.

Проще говоря, метод является инициализирующим класс тогда и только тогда, когда:

    Его имя \u003cclinit\u003e Возвращает void

Следовательно, единственный способ сгенерировать метод \u003cclinit\u003e в Java — это использовать статические поля и инициализаторы статических блоков.

JVM вызывает \u003cclinit\u003e при первом использовании соответствующего класса. Поэтому вызов \u003cclinit\u003e происходит во время выполнения, и мы не можем увидеть вызов на уровне байт-кода.

4. Заключение

В этой быстрой статье мы увидели разницу между методами \u003cinit\u003e и \u003cclinit\u003e в JVM. Метод \u003cinit\u003e используется для инициализации экземпляров объекта. Кроме того, JVM при необходимости вызывает метод \u003cclinit\u003e для инициализации класса.

Чтобы лучше понять, как работает инициализация в JVM, настоятельно рекомендуется прочитать спецификацию JVM.