1. Порівняння record і class: у чому головні відмінності?
У Java є два основні способи описувати власні типи даних: через звичайні класи (class) і через record‑класи (record). На перший погляд обидва варіанти дають змогу зберігати й обробляти дані. Втім, якщо зануритися глибше, відмінностей більше, ніж здається.
Таблиця відмінностей: class vs record
| Характеристика | Звичайний клас (class) | Record‑клас (record) |
|---|---|---|
| Змінюваність | Будь‑яка: поля можуть бути final або ні | Незмінний: усі поля final |
| Успадкування | Можна успадковувати (extends), за замовчуванням не є final | Завжди final, не може бути суперкласом |
| Поля | Будь-які: статичні, нестатичні, final або не‑final, будь-які типи | Лише компоненти record (private final), плюс статичні поля |
| Гетери/сетери | Пишемо самі (або генеруємо за допомогою Lombok) | Гетери створюються автоматично (імʼя поля — імʼя методу), сетерів немає |
| equals/hashCode/toString | Зазвичай пишемо вручну/генеруємо (equals, hashCode, toString) | Генеруються автоматично за всіма компонентами |
| Конструктори | Будь-які, скільки завгодно | Один основний (за всіма компонентами); можна додати компактний конструктор |
| Інтерфейси | Можна реалізовувати | Можна реалізовувати |
| Додаткові методи | Будь-які | Можна додавати, але лише методи (не поля) |
| Використання в колекціях | Можна, але потрібно коректно реалізувати equals/hashCode | Ідеально підходить для ключів і значень, усе вже реалізовано |
Приклад для наочності
Звичайний клас:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Record‑клас:
public record Person(String name, int age) { }
Усе. Один рядок коду — і ви отримуєте той самий функціонал (а подеколи й більше) без ризику щось забути реалізувати.
2. Обмеження record‑класів
Record‑класи — це не просто «короткий синтаксис», а окрема концепція зі своїми суворими правилами. Розгляньмо їх детальніше.
Record завжди final
Record‑клас за визначенням завжди final. Це означає, що ви не можете створити підклас від record:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Помилка компіляції!
Якщо вам потрібно розширити поведінку, використовуйте звичайні класи або композицію (вкладіть record у клас).
Record не може бути батьківським класом
Record‑клас не може бути батьківським класом для інших класів — він завжди final. Це логічно: якби це було можливо, хтось міг би додати змінюване поле — і вся концепція «незмінних даних» зруйнувалася б.
Лише final‑поля (компоненти)
Усі компоненти record оголошуються в заголовку і за замовчуванням private final. Ви не можете додати нестатичні поля до тіла record:
public record User(String login, String email) {
// int counter; // Помилка! Не можна додавати нестатичні поля
static int totalUsers = 0; // Можна, це статичне поле
}
Немає сетерів
Record‑клас не може мати сетерів для компонентів. Будь-яка спроба додати метод на кшталт setX(int x) позбавлена сенсу: ви не зможете змінити значення поля після створення обʼєкта.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Помилка: не можна змінити final‑поле
}
Немає порожнього конструктора
У record‑класі завжди є лише основний конструктор, що приймає значення для всіх компонентів. Неможливо створити record без зазначення всіх даних:
Point p = new Point(1, 2); // ОК
// Point p = new Point(); // Помилка: немає конструктора без параметрів
Немає нестатичних ініціалізаторів
Record‑клас не може містити нестатичні ініціалізатори (ті, що пишуться у фігурних дужках поза методами):
public record User(String login) {
// { /* ... */ } // Помилка: нестатичні ініціалізатори заборонені
}
Обмеження щодо успадкування
Record‑клас не може явно успадковувати інший клас (окрім прихованого базового java.lang.Record). Водночас інтерфейси можна реалізовувати.
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("Друкуємо книгу: " + title);
}
}
Не підходить для складної бізнес‑логіки
record — це про дані, а не про поведінку. Якщо у вашого обʼєкта складна логіка, змінюваний стан, складний життєвий цикл або багато залежностей — record не допоможе. Краще використати звичайний клас.
3. Коли варто використовувати record‑класи?
- DTO (Data Transfer Object): для передавання незмінних даних між шарами застосунку, сервісами, мікросервісами або REST‑контролерами (наприклад, у JSON‑відповідях).
- Value Object: обʼєкти, які визначаються лише своїми значеннями.
- Ключі та значення в колекціях: коли важлива коректна реалізація equals і hashCode (наприклад, для використання в HashMap або Set).
- Результати обчислень: коли потрібно повернути з методу одразу кілька значень (наприклад, record Pair<T, U>(T first, U second)).
Приклад: DTO для REST‑контролера
public record UserDto(String login, String email) { }
Тепер можна безпечно повертати обʼєкт цього типу з контролера, не турбуючись, що хтось змінить його поля.
Приклад: ключ для HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// Усе працює коректно: equals і hashCode уже реалізовано.
4. Коли record‑класи використовувати не варто
- Змінний стан: якщо хоча б одне поле має змінюватися після створення обʼєкта.
- Складна логіка: якщо в обʼєкта складна поведінка, багато методів, вкладені обʼєкти зі змінним станом.
- Успадкування: якщо потрібна ієрархія класів, абстрактні базові класи, перевизначення методів.
- Сутності бізнес‑логіки: наприклад, обʼєкти, що зберігаються в базі даних і мають унікальний ідентифікатор.
Приклад: коли потрібен звичайний клас
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Тут чітко видно, що стан обʼєкта змінюється — record не підходить.
5. Практичні приклади: обираємо між record і class
Приклад 1: record — ідеальний вибір
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- Прямокутник визначається лише шириною та висотою.
- Немає потреби змінювати ці значення після створення.
- Можна додати корисний метод area().
- Усе інше Java зробить за вас.
Приклад 2: class — найкращий варіант
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
Потрібно змінювати розміри прямокутника після створення? Використовуйте звичайний клас.
6. Типові помилки під час роботи з record‑класами
Помилка № 1: спроба додати нестатичне поле.
Record‑клас не дозволяє оголошувати нестатичні поля поза списком компонентів. Якщо спробувати — компілятор видасть помилку. Наприклад:
public record City(String name) {
// int population; // Помилка!
}
Помилка № 2: бажання додати сетер.
Record не підтримує сетерів для компонентів. Будь-яка спроба змінити значення поля після створення обʼєкта — помилка компіляції.
Помилка № 3: спроба успадковувати record або від record.
Record завжди final. Не можна успадковувати від record, і record не може успадковувати інший клас (окрім прихованого java.lang.Record).
Помилка № 4: використання record для змінних обʼєктів.
Якщо ви плануєте змінювати стан обʼєкта після створення — record не для вас. Використовуйте звичайний клас.
Помилка № 5: забули про обмеження конструктора.
У record‑класі обовʼязково має бути конструктор, що приймає значення для всіх компонентів. Конструктора без параметрів немає.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ