Конструкторы Java

Назначение

Конструктор устанавливает начальное корректное состояние объекта. Его имя совпадает с именем класса, а возвращаемого типа нет:

final class Account {
    private final String id;
    private final BigDecimal balance;
 
    Account(String id, BigDecimal balance) {
        this.id = Objects.requireNonNull(id);
        if (balance.signum() < 0) {
            throw new IllegalArgumentException("negative balance");
        }
        this.balance = balance;
    }
}

Если написать void Account(), получится обычный метод, а не конструктор.

Конструктор по умолчанию

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

Его доступ совпадает с доступом самого класса. У класса-наследника неявный конструктор пытается вызвать super(), поэтому родитель должен иметь доступный конструктор без параметров либо наследник должен явно выбрать другой.

Цепочка this и super

Первой инструкцией конструктора может быть ровно один вызов:

  • this(...) — другого конструктора того же класса;
  • super(...) — конструктора родителя.

Если ничего не указано, компилятор вставляет super().

final class Point {
    private final int x;
    private final int y;
 
    Point() {
        this(0, 0);
    }
 
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Цепочка не может быть циклической.

Порядок инициализации

При первом активном использовании класса выполняется его статическая инициализация. Для каждого нового объекта:

  1. выделяется память, поля получают значения по умолчанию;
  2. выполняется конструктор родителя;
  3. выполняются инициализаторы полей и instance-блоки текущего класса в порядке исходного текста;
  4. выполняется тело конструктора текущего класса.

Для многоуровневого наследования этот порядок повторяется от Object к конкретному классу.

class Parent {
    Parent() {
        configure(); // опасно: динамический вызов метода Child
    }
 
    void configure() {}
}
 
class Child extends Parent {
    private String value = "ready";
 
    @Override
    void configure() {
        // value ещё null, если вызов пришёл из Parent()
    }
}

Поэтому из конструктора не вызывают переопределяемые методы и не публикуют this другим потокам.

Фабрики и record

Статическая фабрика может дать имя сценарию, выбрать реализацию или вернуть кэш:

Duration timeout = Duration.ofSeconds(5);

Record автоматически получает канонический конструктор, но в нём можно проверить инварианты:

record Percentage(int value) {
    Percentage {
        if (value < 0 || value > 100) {
            throw new IllegalArgumentException("0..100");
        }
    }
}

Ловушки

  • Путать метод с возвращаемым типом и конструктор.
  • Ожидать автоматический no-args-конструктор после объявления собственного.
  • Дублировать валидацию во всех перегрузках вместо цепочки this.
  • Вызывать переопределяемый метод до завершения инициализации.
  • Сохранять переданную mutable-коллекцию без defensive copy.
  • Выполнять в конструкторе тяжёлый I/O, затрудняя обработку ошибок и тестирование.

Вопросы на собеседовании

  1. Когда компилятор создаёт конструктор без параметров?
  2. В каком порядке вызываются конструкторы родителя и наследника?
  3. Чем отличаются this(...) и super(...)?
  4. Почему опасно вызывать переопределяемый метод из конструктора?
  5. Чем статическая фабрика отличается от конструктора?
  6. Как валидировать инвариант в compact-конструкторе record?

Связанные заметки

Объекты Java · Наследование · Переопределение Java · Работа с исключениями в Java · Immutable классы и данные