У чому основні відмінності Record від звичайного класу
Звичайний клас (class) — це універсальний механізм об'єктно-орієнтованого програмування, призначений для інкапсуляції змінного стану, поліморфної поведінки та бізнес-логіки.
🟢 Junior Level
Record — це спеціалізована форма класу в Java, створена виключно для ролі незмінного носія даних (Data Carrier / Value Object).
Звичайний клас (class) — це універсальний механізм об’єктно-орієнтованого програмування, призначений для інкапсуляції змінного стану, поліморфної поведінки та бізнес-логіки.
Зведена таблиця ключових відмінностей
| Характеристика | record |
Звичайний class |
|---|---|---|
| Призначення | Незмінний контейнер даних | Універсальна сутність (дані + поведінка) |
| Суперклас | Завжди java.lang.Record |
За замовчуванням java.lang.Object (або будь-який клас) |
| Успадкування | Заборонено (неявно final) |
Дозволено (якщо явно не позначений як final) |
| Поля екземпляра | Тільки private final, суворо із заголовка |
Будь-які (public, protected, private, mutable, transient) |
| Аксесори (гетери) | Генеруються автоматично: user.name() |
Пишуться вручну: user.getName() |
equals(), hashCode(), toString() |
Автоматично за всіма полями з коробки | Вимагають ручної реалізації або анотацій Lombok |
| Оголошення | 1 рядок коду | 30–50 рядків коду (конструктор, гетери, методи) |
// Record: лаконічно, безпечно, immutable
public record Point(int x, int y) {}
// Звичайний клас з еквівалентною семантикою вимагає ручного написання всього boilerplate:
public final class PointClass {
private final int x;
private final int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
@Override
public String toString() { ... }
}
🟡 Middle Level
1. Семантичний контракт: «State is the Header»
У звичайному класі зв’язок між полями, параметрами конструктора та гетерами — це суто умовна домовленість (convention), яку підтримує розробник. Поле може називатися x, параметр конструктора — initialX, а гетер може повертати this.x * 2.
У Record діє суворий фундаментальний інваріант мови: стан об’єкта дорівнює його заголовку.
- Не можна додати прихований нестатичний стан:
public record User(String name) { // private int age; // ❌ Помилка компіляції: non-static field not allowed public static int counter; // ✅ Статичні поля дозволені } - Список компонентів у заголовку
(String name, int age)однозначно визначає:- Набір і порядок
private finalполів. - Сигнатуру канонічного конструктора.
- Імена та типи методів-аксесорів (
name(),age()). - Формулу обчислення
equals()таhashCode(). - Формат
toString().
- Набір і порядок
2. Конструктори: канонічний, компактний і кастомний
У звичайному класі конструктор може ініціалізувати поля як завгодно або не ініціалізувати взагалі. У Record діють жорсткі правила:
- Канонічний конструктор: створюється автоматично. Якщо ви перевизначаєте його явно, його параметри зобов’язані точно збігатися із заголовком запису за типами та іменами.
- Компактний конструктор (
Compact Constructor): синтаксис, доступний лише вrecord, створений для валідації та нормалізації параметрів без зайвого повторенняthis.x = x:public record Range(int start, int end) { public Range { if (start > end) { throw new IllegalArgumentException("start > end"); } } } - Кастомні (вторинні) конструктори: зобов’язані першим же рядком викликати канонічний конструктор через
this(...):public record User(String name, int age) { public User(String name) { this(name, 18); // Обов'язкова делегація } }
3. Record і JPA / Hibernate
Часте питання: чи можна замінити звичайні класи сутностей на Records у JPA?
[!IMPORTANT] Records НЕ МОЖНА використовувати як
@Entityу JPA/Hibernate. Специфікація Jakarta Persistence вимагає:
- Наявність конструктора без аргументів (
no-arg constructor), чого у записів немає.- Мутабельність полів або сетери для механізмів відстеження змін (dirty-checking).
- Можливість створення runtime-проксі (CGLIB / ByteBuddy), для чого клас і методи не повинні бути
final(аrecordнеявноfinal).
Де Records ідеально підходять у зв’язці з JPA:
- DTO-проєкції в JPQL:
SELECT new com.example.UserDto(u.id, u.name) FROM User u. - Проєкції Spring Data JPA (
UserRepository.findDtoByName(...)). - Складені первинні ключі (
@Embeddable/@EmbeddedIdв Hibernate 6+).
🔴 Senior Level
Байткод, метадані JVM і Pattern Matching
На рівні платформи Java Record — це не просто трюк компілятора javac (на відміну від Kotlin data class або анотацій Lombok). Це повноцінна мовна концепція, закріплена в специфікації JVM (JVMS):
- Атрибут
Recordу структурі.class: Компілятор записує в байткод масивRecordComponent. Це дає стороннім фреймворкам гарантовану інформацію про структуру об’єкта без необхідності вгадувати за іменами гетерів або читати приватні поля. - Безумовна деконструкція (Record Patterns у Java 21+, JEP 440):
Завдяки прозорості стану
Recordмова підтримує нативне розпакування в операторіswitchіinstanceof:// Деконструкція компонентів прямо в умові: if (obj instanceof Point(int x, int y)) { System.out.println("Coordinates: " + x + ", " + y); } // Зіставлення зі зразком у switch: String describe(Object shape) { return switch (shape) { case Circle(double r) -> "Circle with radius " + r; case Rectangle(double w, double h) when w == h -> "Square: " + w; case Rectangle(double w, double h) -> "Rectangle " + w + "x" + h; default -> "Unknown"; }; }Звичайні класи не підтримують деконструкцію «з коробки», оскільки JVM не може гарантувати взаємно-однозначну відповідність між внутрішнім станом і публічними методами.
Серіалізаційний імунітет
У звичайних класах Java Serialization породжує архітектурну дірку: ObjectInputStream створює об’єкт за допомогою недокументованого native-виклику в обхід конструкторів.
У Record специфікація серіалізації була переписана з нуля:
- Десеріалізація завжди викликає канонічний конструктор.
- Усі інваріанти, оголошені в компактному конструкторі (валідація на
null, діапазон значень), гарантовано виконуються при десеріалізації. - Атаки через підробку серіалізованих байткодів на Records не працюють.
Захист від Reflection
У звичайних класах модифікатор final у полів можна обійти:
// Для звичайного класу (до посилення інкапсуляції модулів):
Field f = NormalClass.class.getDeclaredField("value");
f.setAccessible(true);
f.set(instance, "new value"); // Успішно змінювало final поле!
У випадку з Record метод Field.set() на рівні реалізації ядра JDK містить перевірку:
if (declaringClass.isRecord()) {
throw new IllegalAccessException("Can not set final field of a record class");
}
Поля Record захищені від зміни через Reflection на рівні JVM.
4 Tricky Questions
1. Чому в Record заборонено оголошувати нестатичні (instance) поля в тілі класу, хоча статичні поля дозволені?
Відповідь: Тому що фундаментальний архітектурний інваріант Record стверджує: «Стан запису описується виключно його заголовком».
Якби розробникам дозволили оголошувати додаткові поля екземпляра всередині тіла { ... }, це призвело б до розриву контракту:
- Автогенеровані
equals(),hashCode()іtoString()перестали б враховувати повний стан об’єкта. - Механізм Pattern Matching (деконструкція в
switchіinstanceof) втратив би можливість повністю відновлювати стан об’єкта із заголовка. - Серіалізація не змогла б безпечно відновлювати об’єкт через канонічний конструктор. Статичні поля дозволені, оскільки вони належать самому класу в метаспейсі, а не конкретному екземпляру, і не впливають на його стан.
2. Чи можна використовувати Record у зв’язці з JPA / Hibernate, і якщо так, то в яких сценаріях?
Відповідь:
У якості сутності (@Entity) використовувати Record не можна. Специфікація JPA вимагає наявності конструктора за замовчуванням (no-arg), мутабельності стану (або методів-сетерів) для відстеження змін (dirty-checking), а також відсутності модифікатора final на класі та методах для створення лінивих проксі-об’єктів (CGLIB/ByteBuddy).
Проте Record активно та ефективно застосовується в JPA в таких сценаріях:
- DTO-проєкції: використання в JPQL конструкторних виразах (
SELECT new com.app.UserSummary(u.id, u.name) FROM User u). - Spring Data Projections: повернення Records із методів репозиторіїв (
Optional<UserDto> findUserById(Long id)). - Складені ключі (
@Embeddable/@EmbeddedId): починаючи з Hibernate 6, Records дозволені як компоненти складеного первинного ключа, оскільки для них природно дотримується вимога незмінності, а також коректної реалізаціїequals()таhashCode().
3. Чому методи читання компонентів Record названі field() замість звичного для JavaBeans стандарту getField()?
Відповідь: Творці Java свідомо дистанціювалися від угоди JavaBeans (1996 року).
Стандарт JavaBeans (getX(), setX(), isX()) проєктувався для компонентних візуальних моделей зі змінним станом і генерацією подій зміни властивостей. Record — це не JavaBean, а математичний іменований кортеж (nominal tuple). У ньому немає поняття «property with side-effects», є лише «component value».
Відсутність префікса get:
- Підкреслює функціональну природу об’єкта (метод — це проєкція значення, аналогічно до математичних функцій).
- Уніфікує Java із сучасними мовами (Kotlin, Scala, C#, де також використовується пряме ім’я поля).
- Позбавляє спірних угод на кшталт
getIsActive()vsisActive().
4. Як Record взаємодіє з деконструкцією в Pattern Matching (Java 21 Record Patterns)?
Відповідь: У Java 21 (JEP 440) для Records з’явилася можливість глибокої деконструкції. Оскільки специфікація гарантує відповідність між компонентами заголовка і полями об’єкта, компілятор дозволяє витягувати компоненти прямо в сигнатурі перевірки типів:
record Point(int x, int y) {}
record Line(Point start, Point end) {}
// Глибока вкладена деконструкція:
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
int dx = x2 - x1;
int dy = y2 - y1;
}
Звичайні класи такий синтаксис не підтримують, оскільки для них компілятор не може гарантувати, що виклик довільних методів поверне саме ті значення, з яких було зібрано об’єкт.
🎯 Шпаргалка для інтерв’ю
Головні відмінності в одному погляді
- Семантика: Record — незмінний кортеж («дані як вони є»); Звичайний клас — інкапсульований об’єкт («поведінка + стан»).
- Ієрархія: Record неявно
final, завжди успадковується відjava.lang.Record, пряме успадкування класів заборонено. Інтерфейси дозволені. - Поля: Тільки
private finalіз заголовка. Жодних додаткових полів екземпляра. - Гетери: Іменуються без
get(user.email()). - Специфіка конструкторів: Є компактний конструктор
public User { ... }без круглих дужок для валідації. - Безпека: Захищений від Reflection-модифікацій та обходу конструктора при десеріалізації.
- Java 21: Підтримує Record Patterns (деконструкція в
switchтаinstanceof).
Червоні прапорці (чого НЕ говорити на співбесіді)
- ❌ «Record — це просто синтаксичний цукор для Lombok @Value»: Record — це концепція платформи JVM з атрибутами в байткоді, захищеною десеріалізацією та pattern matching, а не просто генератор коду.
- ❌ «Record можна використовувати як JPA Entity»: JPA вимагає no-arg конструктор, відкритий (non-final) клас для проксі та мутабельність.
- ❌ «У Record можна оголосити змінне поле, якщо позначити його non-final»: компілятор забороняє будь-які поля екземпляра поза заголовком.