Как вызвать конструктор из другого конструктора в Java: разбор под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как вызвать один конструктор из другого в Java: неочевидные детали, которые изменят ваш код

Вы когда-нибудь писали Java-класс с 5–6 конструкторами и чувствовали, что тонете в дублировании кода? Или, наоборот, смотрели на чужой код, где мелькает загадочное this(name, 0, true) и гадали: «А что это за магия?».

Вопрос «How do I call one constructor from another in Java?» — один из самых популярных на Stack Overflow (тысячи просмотров и сотни ответов). И это неспроста. Казалось бы, простая операция — вызов конструктора — таит в себе много нюансов, влияющих на производительность, читаемость и даже корректность работы программы.

В этой статье мы не просто покажем синтаксис. Мы заглянем под капот JVM, разберём, как компилятор обрабатывает такие вызовы, и дадим практические советы, которые пригодятся и новичку, и опытному разработчику.

Зачем вообще вызывать конструктор из конструктора? Проблема дублирования

Представьте, что вы пишете класс User, у которого есть обязательные поля (имя, email) и опциональные (возраст, телефон). Вы хотите дать пользователям вашего API гибкость: создавать объект с минимальным набором данных или со всеми деталями.

Плохой подход (без вызова конструктора):

public class User {
    private String name;
    private String email;
    private int age;
    private String phone;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
        this.age = 0;        // значение по умолчанию
        this.phone = null;   // значение по умолчанию
    }

    public User(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
        this.phone = null;   // опять дублирование
    }

    public User(String name, String email, int age, String phone) {
        this.name = name;
        this.email = email;
        this.age = age;
        this.phone = phone;
    }
}

Здесь мы трижды присваиваем name и email. При добавлении нового поля придётся править все конструкторы. Это нарушает принцип DRY (Don't Repeat Yourself) и увеличивает риск ошибок.

Хороший подход (с вызовом конструктора через this()):

public class User {
    private String name;
    private String email;
    private int age;
    private String phone;

    public User(String name, String email) {
        this(name, email, 0, null);  // делегируем самому "полному" конструктору
    }

    public User(String name, String email, int age) {
        this(name, email, age, null); // снова делегирование
    }

    public User(String name, String email, int age, String phone) {
        this.name = name;
        this.email = email;
        this.age = age;
        this.phone = phone;
    }
}

Теперь вся логика инициализации — в одном конструкторе. Остальные просто делегируют ему работу, передавая значения по умолчанию. Это лаконично, легко поддерживать и меньше шансов забыть про поле.

Синтаксис вызова конструктора: this() и super()

В Java есть два ключевых слова для вызова конструкторов:

  • this(параметры) — вызывает другой конструктор того же класса.
  • super(параметры) — вызывает конструктор родительского класса.

Важнейшее правило: вызов this() или super() должен быть первым оператором в конструкторе. Почему? Об этом чуть позже.

Пример с наследованием:

class Animal {
    private String species;

    public Animal(String species) {
        this.species = species;
        System.out.println("Animal constructor: " + species);
    }
}

class Dog extends Animal {
    private String breed;

    public Dog(String breed) {
        this(breed, "Canis familiaris"); // сначала вызываем другой конструктор Dog
    }

    public Dog(String breed, String species) {
        super(species); // затем вызываем конструктор родителя
        this.breed = breed;
        System.out.println("Dog constructor: " + breed);
    }
}

Обратите внимание: в цепочке вызовов сначала выполняется super(), потом тело конструктора. Это гарантирует, что родительская часть объекта будет инициализирована до дочерней.

Как это работает под капотом: что происходит в байт-коде и JVM

Давайте заглянем под капот. Когда компилятор javac встречает конструктор с this(), он генерирует байт-код, который не создаёт новый объект, а просто передаёт управление другому конструктору того же объекта.

Вот что происходит на уровне JVM:

  1. Выделение памяти: JVM выделяет память под новый объект (это делает оператор new).
  2. Инициализация по умолчанию: все поля получают значения по умолчанию (0, null, false).
  3. Вызов конструктора: начинается выполнение первого конструктора. Его первый оператор — this(...) или super(...).
  4. Цепочка вызовов: если это this(), управление переходит к другому конструктору того же класса. Тот, в свою очередь, вызывает super() (явно или неявно).
  5. Инициализация полей: после возврата из super() выполняются инициализаторы полей (например, private int age = 18;) и блоки { ... }.
  6. Тело конструктора: выполняется код, оставшийся после вызова this()/super().

Почему вызов должен быть первым? Потому что JVM должна гарантировать, что объект полностью инициализирован (включая родительскую часть), прежде чем мы начнём использовать его поля или вызывать методы. Если бы можно было сначала написать this.name = name;, а потом this(...), то после первого присваивания мы бы «перезаписали» объект из другого конструктора — это привело бы к путанице и потенциальным багам.

Интересный факт: компилятор javac на самом деле встраивает код вызываемого конструктора в вызывающий. То есть в байт-коде нет «прыжка» туда-обратно — код просто копируется. Это оптимизация, которая снижает накладные расходы на вызов.

Перегрузка конструкторов и паттерн «Телескопический конструктор»

Техника, когда мы создаём несколько конструкторов с разным количеством параметров, называется перегрузкой конструкторов. Вызов одного из них через this() — это частный случай паттерна «Телескопический конструктор» (Telescoping Constructor Pattern

Блоги

Рекомендаций по книгам