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