Конструкторы 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;
}
}Цепочка не может быть циклической.
Порядок инициализации
При первом активном использовании класса выполняется его статическая инициализация. Для каждого нового объекта:
- выделяется память, поля получают значения по умолчанию;
- выполняется конструктор родителя;
- выполняются инициализаторы полей и instance-блоки текущего класса в порядке исходного текста;
- выполняется тело конструктора текущего класса.
Для многоуровневого наследования этот порядок повторяется от 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, затрудняя обработку ошибок и тестирование.
Вопросы на собеседовании
- Когда компилятор создаёт конструктор без параметров?
- В каком порядке вызываются конструкторы родителя и наследника?
- Чем отличаются
this(...)иsuper(...)? - Почему опасно вызывать переопределяемый метод из конструктора?
- Чем статическая фабрика отличается от конструктора?
- Как валидировать инвариант в compact-конструкторе record?
Связанные заметки
Объекты Java · Наследование · Переопределение Java · Работа с исключениями в Java · Immutable классы и данные