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

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