1. Проблема «класів‑DTO»: навіщо нам record‑класи?
Відверто: скільки разів ви писали ось такий клас?
public class Point {
private final int x;
private final int y;
public Point(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) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" + "x=" + x + ", y=" + y + '}';
}
}
Здається, нічого складного, але порахуйте рядки коду! А тепер уявіть, що у вас 10 таких класів, і в кожному — по 5–6 полів. Навіть IDE втомлюється генерувати цей шаблонний код. А якщо ви вирішите додати нове поле — доведеться редагувати конструктор, гетер, equals, hashCode, toString… Нудно, рутинно — і це джерело помилок.
Такі класи називають DTO (Data Transfer Object) або Value Object. Вони просто зберігають дані — та й годі. Але через шаблонний код їх важко підтримувати.
Якщо вам здається, що це не проблема, просто дочекайтеся моменту, коли доведеться змінювати 50 таких класів одночасно. Тоді ви з особливим теплом згадаєте про record‑класи!
2. Вступ до record: синтаксис і «магія» Java 16+
Починаючи з Java 16, усе змінилося. З’явився новий вид класів — record. Вони спеціально створені для випадків, коли потрібно просто зберігати набір даних. Синтаксис — майже як у кортежах в інших мовах.
Як оголосити record?
public record Point(int x, int y) { }
І… усе! Ви щойно створили незмінюваний клас із двома полями, конструктором, аксесорами, equals, hashCode і toString. Без зайвої писанини.
Що робить Java «за лаштунками»?
- Поля x і y стають private final.
- Аксесори створюються автоматично: int x() і int y().
- Конструктор: public Point(int x, int y).
- equals/hashCode: порівнюють усі поля за значенням.
- toString: повертає рядок на кшталт "Point[x=1, y=2]".
Можна сказати, що record — це «DTO на стероїдах»: менше коду, більше гарантій, менше помилок.
Незмінність (immutability)
Усі поля record‑класу автоматично final. Після створення об’єкта змінити його не можна — це гарантує компілятор.
Якщо ви спробуєте додати сетер або зробити поле не final — компілятор зупинить вас. Така турбота про ваш спокій трапляється нечасто!
3. Приклад використання record‑класу
Звичайний клас (багато коду):
public class Client {
private final String name;
private final int id;
public Client(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() { return name; }
public int getId() { return id; }
// equals, hashCode, toString ...
}
Record‑клас (один рядок!):
public record Client(String name, int id) { }
Використання:
public class Main {
public static void main(String[] args) {
Client client = new Client("Іван", 123);
System.out.println(client.name()); // Іван
System.out.println(client.id()); // 123
System.out.println(client); // Client[name=Іван, id=123]
}
}
Зверніть увагу:
- Методи доступу до полів називаються так само, як поля: name(), id().
- Немає setName() або setId() — об’єкт не можна змінити після створення.
4. Переваги record‑класів: менше коду, менше помилок
Менше коду — більше щастя
Навіщо писати 40 рядків коду, якщо можна обійтися одним? Record‑класи заощаджують час і нерви, особливо у великих проєктах, де багато DTO та value‑об’єктів.
Незмінність «за контрактом»
- Record‑класи завжди final і незмінювані.
- Об’єкт не можна підмінити або випадково змінити.
- Немає «дивних» помилок через зміну стану об’єкта в неочікуваному місці.
- Можна безпечно використовувати у багатопотокових програмах (якщо всі поля також незмінювані).
Автоматична генерація equals/hashCode/toString
Не потрібно вручну писати методи порівняння, обчислення хешу й гарного виведення. Усе робиться автоматично і правильно.
Client c1 = new Client("Анна", 42);
Client c2 = new Client("Анна", 42);
System.out.println(c1.equals(c2)); // true
System.out.println(c1.hashCode() == c2.hashCode()); // true
System.out.println(c1); // Client[name=Анна, id=42]
Ідеально для колекцій і ключів
Record‑об’єкти можна використовувати як ключі в HashMap, елементи в HashSet тощо — усе працюватиме коректно, адже equals і hashCode враховують усі поля.
import java.util.HashMap;
import java.util.Map;
Map<Client, String> clients = new HashMap<>();
clients.put(new Client("Анна", 42), "VIP");
System.out.println(clients.get(new Client("Анна", 42))); // VIP
Чіткий опис даних
Синтаксис record‑класу одразу показує, які дані зберігаються і що об’єкт незмінюваний. Це робить код зрозумілішим для інших розробників (і для вас за пів року).
5. Таблиця: порівняння звичайного класу та record‑класу
| Звичайний клас | Record‑клас | |
|---|---|---|
| Синтаксис | Багато коду | Один рядок |
| Незмінюваність | Потрібно визначати вручну | Гарантована компілятором |
| Автогенерація методів | Ні | Так (equals, hashCode, toString) |
| Можна додати поля | Так | Лише компоненти record |
| Успадкування | Можна успадковувати | Завжди final, успадковувати не можна |
| Використання у колекціях | Потрібно коректно реалізувати методи | Працює без додаткового коду |
6. Розвиваємо навчальний застосунок: приклад з record‑класом
Припустімо, у вашому навчальному банківському застосунку потрібно зберігати операції за рахунком: дата, сума, тип операції (наприклад, «поповнення» або «зняття»).
До Java 16:
public class Transaction {
private final LocalDate date;
private final double amount;
private final String type;
public Transaction(LocalDate date, double amount, String type) {
this.date = date;
this.amount = amount;
this.type = type;
}
public LocalDate getDate() { return date; }
public double getAmount() { return amount; }
public String getType() { return type; }
// equals, hashCode, toString ...
}
Починаючи з Java 16 — record:
import java.time.LocalDate;
public record Transaction(LocalDate date, double amount, String type) { }
Використання:
Transaction t = new Transaction(LocalDate.now(), 100.0, "deposit");
System.out.println(t); // Transaction[date=2024-06-01, amount=100.0, type=deposit]
System.out.println(t.amount()); // 100.0
7. Візуалізація: що генерує record
Погляньмо на «розгорнутий» record‑клас (приблизно те, що згенерує компілятор):
public final class Point extends java.lang.Record {
private final int x;
private final int y;
public Point(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() { /* гарне виведення */ }
}
Коротко: коли варто використовувати record
- Коли потрібен незмінюваний об’єкт із набором даних.
- Коли потрібен «чесний» equals/hashCode/toString без ручної писанини.
- Коли ви робите DTO, Value Object, пари, трійки, колір, точку, діапазон, ключ для колекції тощо.
8. Типові помилки під час роботи з record‑класами
Помилка № 1: спроба додати сетер або змінити поле після створення.
Record‑клас не дозволяє змінювати свої поля. Якщо ви спробуєте додати метод на кшталт setX(int x), компілятор одразу скаже «не можна». Те саме — якщо спробувати змінити поле напряму.
Помилка № 2: спроба додати нестатичне поле.
У record‑класі можна оголошувати лише компоненти (поля, вказані у дужках після імені record) і статичні поля. Звичайні нестатичні поля додавати не можна — компілятор цього не дозволить.
Помилка № 3: використання record для логіки зі змінним станом.
Record‑класи не призначені для об’єктів зі змінним станом. Якщо вам потрібно щось змінювати після створення — використовуйте звичайний клас.
Помилка № 4: забули, що record завжди final.
Record‑клас не можна успадковувати й не можна зробити його суперкласом. Спроба порушити це обмеження призведе до помилки компіляції. Ключове: не намагайтеся розширювати record — його задумано як завершений незмінюваний тип.
Помилка № 5: ігнорування автогенерованих методів.
Якщо ви перевизначаєте equals, hashCode або toString, будьте обережні — не порушуйте їхній контракт, інакше колекції та порівняння працюватимуть некоректно.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ