Що таке компактний конструктор (compact constructor) в Record
Головна синтаксична особливість компактного конструктора — відсутність круглих дужок зі списком параметрів і відсутність ручного присвоєння полів (this.x = x):
🟢 Junior Level
Компактний конструктор (Compact Constructor) — це спеціалізована форма запису канонічного конструктора в Record, створена спеціально для валідації інваріантів та нормалізації вхідних даних.
Головна синтаксична особливість компактного конструктора — відсутність круглих дужок зі списком параметрів і відсутність ручного присвоєння полів (this.x = x):
public record User(String name, int age) {
// Компактний конструктор: тільки ім'я типу та фігурні дужки!
public User {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
}
// Нормалізація даних:
name = name.trim();
}
}
Порівняння зі звичайним конструктором
// Класичний конструктор (багато зайвого шаблонного коду):
public User(String name, int age) {
if (age < 0) throw new IllegalArgumentException();
this.name = name.trim(); // Ручне присвоєння кожному полю
this.age = age;
}
// Компактний конструктор (чистий фокус на бізнес-правилах):
public User {
if (age < 0) throw new IllegalArgumentException();
name = name.trim();
// Присвоєння this.name = name та this.age = age компілятор зробить САМ!
}
🟡 Middle Level
1. Механізм десугаризації компілятора
Компактний конструктор — це не окремий метод у байткоді, а синтаксичний цукор для тіла канонічного конструктора.
При компіляції javac виконує таку трансформацію:
- Створює параметри методу, які в точності збігаються з компонентами запису:
(String name, int age). - Розміщує код валідації та трансформації, написаний розробником, на самому початку методу.
- У самий кінець методу автоматично додає байткод присвоєння:
this.name = name; this.age = age;
[Вхідні аргументи] ──> [Тіло компактного конструктора] ──> [Автоматичний this.field = param] ──> Об'єкт створено!
(Валідація, нормалізація)
2. Мутація параметрів перед збереженням
У компактному конструкторі змінні компонентів (name, age) являють собою локальні параметри методу, а не поля об’єкта. Їх можна переприсвоювати:
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount);
Objects.requireNonNull(currency);
// Округлюємо до 2 знаків перед збереженням у final поле:
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
}
3. Захисне копіювання колекцій
Якщо record містить колекцію, компактний конструктор — ідеальне місце для створення незмінної копії (захист від мутацій ззовні):
public record Order(String id, List<OrderItem> items) {
public Order {
Objects.requireNonNull(id);
// Захисне копіювання: List.copyOf створює по-справжньому незмінний список
items = List.copyOf(items);
}
}
🔴 Senior Level
Правила специфікації мови (JLS §8.10.4.2)
Специфікація мови Java накладає суворі правила на тіло компактного конструктора:
1. Заборона звернення до this.field
Спроба явного читання або запису поля через this всередині компактного конструктора призведе до помилки компіляції:
public record Point(int x, int y) {
public Point {
// ❌ Помилка компіляції: cannot assign a value to final variable x
this.x = x;
// ❌ Помилка компіляції: variable x might not have been initialized
System.out.println(this.x);
// ✅ Правильно: оперувати неявним параметром
x = Math.max(0, x);
}
}
Чому? Поля запису є final, і компілятор суворо гарантує їхнє визначене присвоєння (definite assignment) рівно один раз — у синтезованому хвості конструктора.
2. Заборона секції throws
Компактний конструктор не має сигнатури, тому на ньому не можна оголосити перевірювані винятки (throws CheckedException):
public record Config(String path) {
// ❌ Помилка компіляції: throws clause not allowed on compact constructor
public Config throws IOException { ... }
}
Будь-які помилки ініціалізації зобов’язані викидатися у вигляді неперевірюваних винятків (IllegalArgumentException, NullPointerException, IllegalStateException).
3. Нерозривний зв’язок із Java Serialization
У звичайних класах Java десеріалізація (ObjectInputStream.readObject) створює екземпляр через Unsafe, повністю ігноруючи конструктори класу. Це історично дозволяло зловмисникам створювати об’єкти з невалідним станом в обхід перевірок безпеки.
Для Record специфікація серіалізації жорстко вимагає:
- Значення компонентів витягуються з потоку байтів.
- Потік зобов’язаний викликати канонічний конструктор запису.
- Код компактного конструктора гарантовано виконується при десеріалізації!
Якщо зловмисник підробить серіалізований потік і передасть від’ємний вік або шкідливий рядок, компактний конструктор викине IllegalArgumentException, і десеріалізація буде заблокована.
4 Tricky Questions
1. Що станеться, якщо в тілі компактного конструктора спробувати прочитати або записати поле через this.name?
Відповідь: Компілятор видасть помилку компіляції.
У компактному конструкторі доступ до стану об’єкта через посилання this заблокований: поля екземпляра ще не ініціалізовані, і їхня ініціалізація перебуває в зоні відповідальності самого компілятора. Ідентифікатор name у тілі компактного конструктора посилається не на поле класу, а на неявний вхідний аргумент конструктора. Розробник може читати та перезаписувати цей аргумент (name = name.toUpperCase()), але звернення до this.name неприпустимо.
2. Чи виконується код компактного конструктора при десеріалізації Record через стандартний ObjectInputStream?
Відповідь: Так, виконується в обов’язковому порядку.
Це фундаментальна архітектурна відмінність Records від звичайних класів. Для звичайних класів серіалізація обходить конструктори за допомогою ReflectionFactory.newConstructorForSerialization, створюючи дірку в безпеці.
Специфікація Java Serialization для Record приписує віртуальній машині відновлювати компоненти з потоку та передавати їх аргументами в канонічний конструктор. Оскільки компактний конструктор десугаризується в тіло канонічного, всі перевірки інваріантів та нормалізації гарантовано відпрацьовують при десеріалізації.
3. Чи можна повісити анотації на компактний конструктор (наприклад, @Deprecated або валідаційні анотації)?
Відповідь: Так, можна.
На компактний конструктор можна розміщувати будь-які анотації, цільовим елементом яких є конструктор (@Target(ElementType.CONSTRUCTOR)), наприклад @Deprecated або кастомні маркери. Компілятор перенесе ці анотації на згенерований у байткоді канонічний конструктор.
4. В якому порядку відпрацьовують вторинний конструктор, компактний конструктор та фабричні методи?
Відповідь: Якщо об’єкт створюється через вторинний конструктор:
- Виконуються вирази обчислення аргументів у виклику
this(arg1, arg2). - Керування переходить у канонічний конструктор.
- Насамперед виконується тіло компактного конструктора (валідація інваріантів та нормалізація параметрів).
- Компілятор виконує неявне присвоєння полів
this.comp = param. - Канонічний конструктор завершує роботу, і керування повертається у вторинний конструктор для виконання решти інструкцій його тіла (якщо вони є).
🎯 Шпаргалка для інтерв’ю
Компактний конструктор за 30 секунд
- Синтаксис:
public User { ... }— без круглих дужок і без параметрів. - Головна мета: Валідація інваріантів (
if (x < 0) throw...) та нормалізація даних (str = str.trim()). - Як влаштований під капотом: Код вбудовується компілятором у канонічний конструктор ПЕРЕД неявним присвоєнням
this.field = param. - Правило
this: Заборонено писатиthis.field = ...усередині компактного конструктора. Мутувати можна тільки локальні параметри. - Винятки: Лише unchecked (
RuntimeException). Секціяthrowsзаборонена. - Безпека: Компактний конструктор викликається навіть при десеріалізації через
ObjectInputStream, запобігаючи ін’єкціям пошкоджених даних.
Антипатерни
- ❌ Спроба ручного присвоєння
this.x = x: помилка компіляції. - ❌ Захисне копіювання через мутабельний
new ArrayList<>(list): порушує незмінність, використовуйтеList.copyOf(list). - ❌ Оголошення одночасно компактного та канонічного конструктора: призведе до помилки дублювання методу.