Что такое Record и как он помогает создавать иммутабельные классы
Объявление record в одну строку полностью заменяет 30+ строк шаблонного кода (boilerplate):
🟢 Junior Level
30-секундный ответ
Запись (Record) — это специализированный вид класса, появившийся как превью в Java 14 и ставший стабильным стандартом в Java 16 (JEP 395). Он спроектирован специально как «прозрачный переносчик неизменяемых данных» (Transparent Carrier for Immutable Data).
Объявление record в одну строку полностью заменяет 30+ строк шаблонного кода (boilerplate):
// Одна строка вместо целого класса!
public record GeoPoint(double latitude, double longitude) {}
Компилятор javac автоматически генерирует в байткоде:
- Класс со спецификатором
final, неявно наследующийjava.lang.Record. - Поля компонентов со спецификаторами
private final. - Канонический конструктор со всеми параметрами.
- Публичные методы чтения без префикса
get(в примере:latitude()иlongitude()). - Корректные реализации
equals(),hashCode()иtoString(), учитывающие все компоненты записи.
Сравнение: Обычный класс vs Record
// ❌ ДО Java 16: Многословный boilerplate
public final class PointOld {
private final int x;
private final int y;
public PointOld(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
// ✅ В Java 16+: Идеальная лаконичность
public record Point(int x, int y) {}
🟡 Middle Level
Главная ловушка: Поверхностная иммутабельность (Shallow Immutability)
record гарантирует, что ссылки на поля нельзя перезаписать. Однако если компонент записи является изменяемым объектом (коллекция, массив, Date), его содержимое можно свободно модифицировать извне:
public record UserGroup(String groupName, List<String> members) {}
// ОПАСНОСТЬ:
List<String> mutableList = new ArrayList<>();
mutableList.add("Alice");
UserGroup group = new UserGroup("Admins", mutableList);
// Модификация исходного списка ломает состояние Record!
mutableList.add("Hacker");
// Модификация через геттер:
group.members().clear(); // Содержимое группы очищено!
Решение: Компактный конструктор (Compact Constructor)
В record появился специальный синтаксис — компактный конструктор. В нём отсутствуют круглые скобки со списком параметров, а присвоение полям this.x = x выполняется компилятором автоматически в самом конце:
public record UserGroup(String groupName, List<String> members) {
// Компактный конструктор для валидации и защитного копирования:
public UserGroup {
Objects.requireNonNull(groupName, "Group name cannot be null");
// Защитная копия: превращаем в неизменяемый список!
members = (members == null) ? List.of() : List.copyOf(members);
}
}
Виды конструкторов в Record
- Канонический (Canonical): конструктор, сигнатура которого совпадает с заголовком записи. Генерируется неявно.
- Компактный (Compact): модифицирует параметры или валидирует их до неявного присвоения полям.
- Кастомный (Пользовательский): обязан явно делегировать вызов каноническому конструктору через
this(...):public record Point(int x, int y) { // Дополнительный конструктор: public Point(int value) { this(value, value); // Обязательный вызов this(...) } }
🔴 Senior Level
Безопасность десериализации (Safe Deserialization)
Классическая сериализация Java (implements Serializable) исторически являлась главным источником уязвимостей (RCE-эксплойты, обход валидаций в конструкторах):
- При десериализации обычного класса JVM использует «магический» рефлексивный механизм, который полностью обходит конструкторы класса, напрямую восстанавливая байты в поля.
- Records кардинально меняют архитектуру сериализации (JEP 395):
При десериализации
recordJVM обязана вызвать канонический конструктор! Все проверки бизнес-логики, ограничения и защитные копии, написанные в компактном конструкторе, гарантированно выполнятся даже при десериализации. Создать невалидный Record через манипуляцию байтами сериализации невозможно.
Pattern Matching и деконструкция Record (Java 21, JEP 440)
В Java 21 записи стали основой для паттерн-матчинга с деконструкцией (Record Patterns):
public sealed interface Shape permits Circle, Rectangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
// Деконструкция прямо в блоке switch без явных вызовов геттеров:
public static double calculateArea(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
};
}
Архитектурные ограничения Record
- Запрет наследования классов:
recordуже неявно наследуется отjava.lang.Recordи являетсяfinal. Он не может наследоваться от другого класса и не может служить суперклассом. - Интерфейсы разрешены:
recordможет реализовывать любое количество интерфейсов (implements Serializable, Comparable<T>). - Запрет дополнительных полей экземпляра: внутри тела
recordнельзя объявлять нестатические поля:public record User(String name) { // private int age; // ❌ ОШИБКА КОМПИЛЯЦИИ! public static final String DEFAULT_ROLE = "GUEST"; // ✅ Статические поля разрешены }
4 Tricky Questions
1. Почему десериализация Record в Java считается намного безопаснее десериализации обычных классов?
Ответ:
Для стандартных классов механизм десериализации Java восстанавливает поля напрямую из входного потока байтов в память в обход конструкторов (используя нативный бэкдор ReflectionFactory.newConstructorForSerialization). В результате вредоносный байт-код может создать объект с невалидными инвариантами или внедрить эксплойт.
Для record спецификация JVM требует обязательного вызова канонического конструктора при десериализации:
- Поток байтов читает значения компонентов записи.
- Аргументы передаются в канонический конструктор.
- Компактный конструктор выполняет валидацию (
Objects.requireNonNull, диапазоны чисел) и защитное копирование коллекций. Если данные подделаны, конструктор выбросит исключение, предотвратив создание скомпрометированного объекта.
2. Можно ли переопределить метод доступа (геттер) в record? Не сломает ли это контракт записи?
Ответ: Да, метод доступа можно явно переопределить в теле записи:
public record User(String name) {
@Override
public String name() {
return name == null ? "Anonymous" : name.trim();
}
}
Однако спецификация Java настойчиво рекомендует: геттер должен оставаться чистой проекцией состояния компонента. Если переопределить геттер так, чтобы он возвращал совершенно другое значение, методы equals(), hashCode() и toString() (которые по умолчанию читают приватные поля напрямую, а не через геттеры) разойдутся в поведении с вызовом метода доступа, что нарушит принцип прозрачности данных.
3. Может ли record быть объявлен локально внутри метода? Имеет ли он доступ к локальным переменным метода?
Ответ:
Да, начиная с Java 16, записи можно объявлять локально внутри методов (Local Records).
Однако важный нюанс: локальный Record является неявно статическим (static).
Он не имеет права захватывать нестатические переменные обрамляющего метода (в отличие от локальных анонимных классов). Все данные, с которыми работает локальный Record, должны передаваться ему явно через аргументы конструктора. Это исключает скрытые утечки памяти стека.
4. В чём разница между Record и аннотацией Lombok @Value?
Ответ:
- Уровень реализации:
record— это фундаментальная языковая конструкция и фича JVM. Она поддерживается байткодом, Reflection API (Class.isRecord(),Class.getRecordComponents()), десериализацией и Record Patterns в Java 21. Lombok@Value— это хак компилятора через Annotation Processor, просто генерирующий обычный Java-класс. - Семантика данных:
recordобязан быть финальным, не допускает скрытых полей состояния и гарантирует вызов конструктора при десериализации. Lombok@Valueпозволяет создавать изменяемые сущности или наследовать структуры, но не имеет нативной поддержки со стороны виртуальной машины.
🎯 Шпаргалка для интервью
Главное о Java Record
- Введён: Java 14 (preview), Java 16 (LTS / стандарт).
- Назначение: неизменяемый контейнер данных с минимальным синтаксисом.
- Генерирует:
final class,private finalполя, конструктор, геттеры без префиксаget(x()),equals(),hashCode(),toString(). - Иммутабельность: поверхностная (Shallow)! Мутабельные коллекции требуют обязательного защитного копирования (
List.copyOf) в компактном конструкторе. - Ограничения: запрещено наследование классов, запрещены дополнительные поля экземпляра.
Сравнение синтаксиса геттеров
| Обычный класс / JavaBeans | Java Record |
| :— | :— |
| user.getName() | user.name() |
| user.isActive() | user.active() |
Красные флаги на собеседовании (Чего говорить нельзя)
- ❌ «Record полностью защищает коллекции от изменений» — Record создаёт shallow-иммутабельность;
list.add()всё ещё сработает, если не сделатьList.copyOf(). - ❌ «Геттеры в Record называются getName() и getAge()» — префикса
getнет: методы называютсяname()иage(). - ❌ «Record может наследоваться от абстрактного класса» — запрещено, все рекорды уже неявно наследуют
java.lang.Record. - ❌ «Record — это просто синтаксический сахар для Lombok @Data» —
@Dataгенерирует мутабельный класс с сеттерами, а Record — иммутабельный тип с поддержкой в JVM и pattern matching.