📋 Розділ 20 · Питання #7

Що таке компактний конструктор (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 виконує таку трансформацію:

  1. Створює параметри методу, які в точності збігаються з компонентами запису: (String name, int age).
  2. Розміщує код валідації та трансформації, написаний розробником, на самому початку методу.
  3. У самий кінець методу автоматично додає байткод присвоєння:
    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 специфікація серіалізації жорстко вимагає:

  1. Значення компонентів витягуються з потоку байтів.
  2. Потік зобов’язаний викликати канонічний конструктор запису.
  3. Код компактного конструктора гарантовано виконується при десеріалізації!

Якщо зловмисник підробить серіалізований потік і передасть від’ємний вік або шкідливий рядок, компактний конструктор викине 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. В якому порядку відпрацьовують вторинний конструктор, компактний конструктор та фабричні методи?

Відповідь: Якщо об’єкт створюється через вторинний конструктор:

  1. Виконуються вирази обчислення аргументів у виклику this(arg1, arg2).
  2. Керування переходить у канонічний конструктор.
  3. Насамперед виконується тіло компактного конструктора (валідація інваріантів та нормалізація параметрів).
  4. Компілятор виконує неявне присвоєння полів this.comp = param.
  5. Канонічний конструктор завершує роботу, і керування повертається у вторинний конструктор для виконання решти інструкцій його тіла (якщо вони є).

🎯 Шпаргалка для інтерв’ю

Компактний конструктор за 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).
  • ❌ Оголошення одночасно компактного та канонічного конструктора: призведе до помилки дублювання методу.

Пов’язані теми