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

Чи можна перевизначити конструктор в Record

Будь-який вторинний конструктор зобов'язаний делегувати ініціалізацію іншому конструктору через this(...), доки ланцюжок не дійде до канонічного конструктора. Виклик super(...)...


🟢 Junior Level

Так, можна. У Java для Record підтримуються три форми конструкторів:

  1. Компактний конструктор (Compact Constructor): спеціалізована форма без круглих дужок зі списком аргументів, призначена для валідації та нормалізації даних:
    public record User(String name, int age) {
        public User {
            if (age < 0) {
                throw new IllegalArgumentException("Age cannot be negative: " + age);
            }
            name = name.trim(); // нормалізація
        }
    }
    
  2. Явний канонічний конструктор: повне оголошення конструктора з усіма параметрами із заголовка:
    public record Point(int x, int y) {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    
  3. Вторинні (додаткові) конструктори: конструктори з іншим набором параметрів, які в обов’язковому порядку першим же рядком викликають канонічний конструктор через this(...):
    public record User(String name, int age) {
        public User(String name) {
            this(name, 18); // Обов'язковий виклик this(...)
        }
    }
    

Головне правило

Будь-який вторинний конструктор зобов’язаний делегувати ініціалізацію іншому конструктору через this(...), доки ланцюжок не дійде до канонічного конструктора. Виклик super(...) у записах заборонено.


🟡 Middle Level

1. Як влаштований компактний конструктор

Компактний конструктор — це унікальна фіча Record. Він оголошується без списку параметрів: public User { ... }.

Що робить компілятор під капотом:

  1. Неявно передає в тіло конструктора параметри, що відповідають компонентам заголовка (name, age).
  2. Вставляє код валідації, написаний розробником.
  3. У самому кінці методу автоматично вставляє фінальне присвоєння полів:
    this.name = name;
    this.age = age;
    
public record NormalizedEmail(String value) {
    public NormalizedEmail {
        Objects.requireNonNull(value, "Email must not be null");
        // Змінна value тут — це не поле, а неявний параметр методу!
        value = value.toLowerCase().trim();
        // Наприкінці компілятор автоматично зробить: this.value = value;
    }
}

[!WARNING] Усередині компактного конструктора заборонено звертатися до полів через this.field! Спроба написати this.value = value; викличе помилку компіляції: «Cannot assign a value to final variable value».

2. Захисне копіювання (Defensive Copy) у конструкторі

Якщо компонент Record — мутабельна колекція або масив, ініціалізація повинна створювати захисну незмінну копію:

public record Team(String name, List<String> members) {
    public Team {
        Objects.requireNonNull(name);
        // Захист від мутацій: створюємо по-справжньому незмінний список
        members = List.copyOf(members);
    }
}

🔴 Senior Level

Правила специфікації мови (JLS §8.10.4)

Специфікація мови Java накладає суворі правила на конструктори записів:

1. Заборона одночасного оголошення компактного та явного канонічного конструктора

В одному record не можна оголосити і компактний, і явний канонічний конструктор:

public record Point(int x, int y) {
    public Point { ... } // Компактний
    // ❌ Помилка компіляції: duplicate method Point(int, int)
    public Point(int x, int y) { ... } 
}

Компактний конструктор компілюється безпосередньо в тіло канонічного конструктора. Наявність обох призводить до конфлікту дублювання методу з однаковою сигнатурою.

2. Обмеження модифікаторів доступу

Модифікатор доступу канонічного (або компактного) конструктора не може бути суворішим, ніж у самого Record:

public record User(String name) {
    // ❌ Помилка компіляції: canonical constructor cannot have more restrictive access than the record class
    private User(String name) {
        this.name = name;
    }
}

Якщо клас public, то канонічний конструктор зобов’язаний бути public. Якщо клас package-private, конструктор може бути public або package-private.

Проте вторинні конструктори можуть мати будь-який модифікатор, включаючи private:

public record Token(String value) {
    // Канонічний конструктор неявно public
    
    // Вторинний конструктор може бути private:
    private Token(byte[] raw) {
        this(Base64.getEncoder().encodeToString(raw));
    }

    public static Token fromBytes(byte[] raw) {
        return new Token(raw);
    }
}

3. Семантика винятків (Throws Clause)

  • Компактний конструктор не може містити секцію throws:
    public record FileData(String path) {
        // ❌ Помилка компіляції: throws clause not allowed on compact constructor
        public FileData throws IOException { ... }
    }
    

    Будь-які помилки в компактному конструкторі мають викидатися як RuntimeException (наприклад, IllegalArgumentException, NullPointerException).

  • Явний канонічний конструктор може оголосити throws CheckedException, але це вважається грубим антипатерном, оскільки ламає функціональне створення об’єктів і десеріалізацію.

4 Tricky Questions

1. Чи можна в одному Record оголосити одночасно компактний конструктор і явний канонічний конструктор?

Відповідь: Ні, це призведе до помилки компіляції.

Компактний конструктор — це просто альтернативна синтаксична форма запису канонічного конструктора. На етапі генерації байткоду компілятор створює з компактного конструктора звичайний конструктор із сигнатурою (ComponentType1, ComponentType2, ...). Спроба оголосити обидва конструктори одночасно означає створення двох методів з ідентичною сигнатурою, що викличе помилку компілятора: «Variable-arity method or duplicate method in type».

2. Чи може канонічний конструктор Record мати модифікатор доступу суворіший, ніж сам клас (наприклад, private конструктор у public record)?

Відповідь: Ні, не може.

Згідно зі специфікацією JLS §8.10.4, модифікатор доступу канонічного конструктора має бути не менш доступним, ніж модифікатор доступу самого класу Record. Для public record канонічний конструктор зобов’язаний бути public. Ця вимога обумовлена тим, що Record спроєктований як прозорий контейнер даних: будь-який код, що має доступ до типу запису, повинен мати право сконструювати його з компонентів.

Вторинні (неканонічні) конструктори при цьому можуть бути private.

3. Чи дозволено всередині компактного конструктора звертатися до полів через this.field (наприклад, this.x = 10)?

Відповідь: Ні, звернення до полів через this. всередині компактного конструктора заборонено компілятором.

У компактному конструкторі код розробника виконується до ініціалізації фінальних полів об’єкта. Ідентифікатори компонентів (x, y) у тілі компактного конструктора посилаються на неявні локальні параметри, а не на поля екземпляра. Ви можете перевизначити значення параметру (x = Math.max(0, x);), і компілятор сам згенерує код this.x = x; у самому кінці конструктора. Пряма спроба читання або запису this.x викличе помилку компіляції.

4. Чи можна у вторинному конструкторі Record викликати super(...) замість this(...)?

Відповідь: Категорично ні.

Вторинний конструктор Record зобов’язаний першим рядком викликати інший конструктор цього ж класу через this(...). Прямий виклик super(...) заборонений. Єдиним суперкласом для записів є java.lang.Record, чий конструктор викликається компілятором автоматично лише з канонічного конструктора.


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

3 види конструкторів у Record

Тип конструктора Сигнатура Особливості
Компактний public User { ... } Без параметрів. Валідація та нормалізація. Присвоєння полів приховано.
Канонічний public User(String name, int age) { ... } Повний список параметрів. Ручне присвоєння this.x = x.
Вторинний public User(String name) { this(name, 0); } Будь-які параметри. Зобов’язаний викликати this(...).

Головні інваріанти

  1. Або компактний, або канонічний конструктор — але ніколи обидва одразу.
  2. У компактному конструкторі: пишемо name = name.trim(), але не this.name = ....
  3. Модифікатор доступу канонічного конструктора не може бути вужчим, ніж у класу.
  4. Виклик super(...) заборонено — лише this(...).
  5. Для захисного копіювання мутабельних колекцій використовуйте List.copyOf(), а не new ArrayList<>().

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