Що таке 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.