1. Record-класи та незмінність
Незмінність — це властивість обʼєкта, за якої його стан не можна змінити після створення. Іншими словами, якщо обʼєкт створено, ви не можете змінити його внутрішні дані. Усе — наче висічено в камені.
Уявіть квиток на поїзд. Щойно його надруковано, ви не можете змінити дату або місце відправлення (якщо не брати до уваги маніпуляції у Photoshop). Квиток — незмінний обʼєкт. Потрібен інший квиток — купуєте новий.
У програмуванні такі обʼєкти називають immutable objects. Вони захищають програму від випадкових змін, роблять код безпечнішим і передбачуванішим.
Ознаки незмінного обʼєкта
- Усі поля обʼєкта — final (їх можна присвоїти лише один раз, зазвичай у конструкторі).
- Немає сеттерів (методів, що змінюють значення полів).
- Методи, які повертають внутрішні дані, або повертають їхні копії, або повертають самі дані, які також є незмінними.
Record-класи в Java створені для того, щоб зручно описувати незмінні структури даних.
Чому record є незмінним?
- Усі компоненти record — final.
Коли ви оголошуєте record, компілятор автоматично робить усі його поля private final. Це означає, що після створення обʼєкта ви не зможете змінити його поля. - Немає сеттерів.
У record-класі не можна додати метод setX(int x) — компілятор не дозволить змінити значення поля після створення обʼєкта. - Конструктор присвоює значення лише один раз.
Усі значення задаються лише під час створення обʼєкта.
Приклад
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // Помилка компіляції: поле x має private-доступ і є final
// p.x(7); // Помилка: немає сеттера!
System.out.println(p.x()); // 5
Спроба змінити поле або викликати неіснуючий сеттер призводить до помилки компіляції. Java суворо дотримується контракту незмінності.
2. Переваги незмінних обʼєктів
Безпека у багатопоточності
У багатопоточних програмах (а таких зараз більшість!) незмінні обʼєкти — це як бронежилет. Якщо обʼєкт не можна змінити, різні потоки можуть його спокійно читати, не боячись, що хтось у цей момент змінить дані. Не потрібно синхронізувати доступ і перейматися через гонки даних.
Факт: багато класів у стандартній бібліотеці Java, які активно використовують у багатопоточних сценаріях, або незмінні, або спеціально захищені від змін.
Спрощення розуміння коду
Якщо обʼєкт незмінний, ви завжди впевнені: передали його в інший метод або клас — і він не зміниться «у вас за спиною». Це значно полегшує читання й налагодження коду. Не потрібно гадати, хто і де міг змінити поле, — ніхто не міг!
Зручно використовувати як ключі в колекціях
Незмінні обʼєкти чудово підходять для використання як ключі в колекціях на кшталт HashMap або HashSet. Чому? Тому що їхні equals і hashCode залежать лише від полів, які не змінюються. Отже, обʼєкт не «загубиться» в колекції через те, що його стан змінився.
Менше прихованих багів
Змінний обʼєкт легко зіпсувати, випадково передавши посилання не туди. Незмінний же обʼєкт — як друкована книга: ніхто не може вирвати або переписати сторінку.
3. Порівняння зі звичайними класами
Порівняймо поведінку звичайного класу та record-класу. Для прикладу візьмемо просту модель точки на площині.
Звичайний клас (mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
Можна створити обʼєкт і потім скільки завгодно змінювати його стан:
PointClass p = new PointClass(1, 2);
p.setX(10); // p.x тепер 10
Record-клас (immutable)
public record Point(int x, int y) {}
Створюєте обʼєкт — і все: він назавжди залишається таким, яким його створено:
Point p = new Point(1, 2);
// p.x = 10; // Помилка! Немає доступу до поля
// p.x(10); // Помилка! Немає сеттера
Таблиця: порівняння поведінки
| Звичайний клас | Record-клас | |
|---|---|---|
| Поля | будь-які | лише private final |
| Сеттери | можна додати | не можна додати |
| Змінюваність | змінюваний (mutable) | незмінний |
| Автогенерація | ні | так (equals, hashCode, toString) |
4. Практика: як використовувати незмінність record-класів
Спробуймо застосувати record-клас у невеликому прикладі. Нехай у нас є банківський застосунок, і ми хочемо зберігати інформацію про транзакцію:
public record Transaction(String fromAccount, String toAccount, double amount) {}
Створюємо обʼєкт:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
Спробуємо «переказати» гроші на інший рахунок:
// t.toAccount = "11111"; // Помилка! Поле final, доступу немає
// t.toAccount("11111"); // Помилка! Немає сеттера
Якщо потрібна інша транзакція — створюємо новий обʼєкт:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
Важливо: незмінність не означає «незручно». Це просто інший стиль роботи: якщо потрібен новий стан — створюємо новий обʼєкт.
5. Особливості незмінності: що потрібно памʼятати
Незмінність — не завжди абсолютна!
Record-клас гарантує, що його поля не зміняться. Але якщо поле — це посилання на змінюваний обʼєкт (наприклад, масив або звичайний клас), то вміст цього обʼєкта може бути змінений.
Приклад із масивом
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99! Масив змінився
Висновок: якщо хочете справжню незмінність, використовуйте лише незмінні типи (String, Integer, інші record-класи тощо) або робіть копії змінюваних обʼєктів усередині канонічного конструктора record-класу. Наприклад:
int[] copy = Arrays.copyOf(data, data.length);
6. Як самостійно зробити звичайний клас незмінним
Якщо ви хочете зробити звичайний клас immutable (незмінним), доведеться вручну:
- Позначити всі поля як private final,
- Не додавати сеттерів,
- Усі поля ініціалізувати лише через конструктор,
- Якщо поле — змінюваний обʼєкт, робити захисну копію (defensive copy).
Приклад
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
Погодьтеся, з record-класом це робиться простіше й коротше:
public record User(String name, int age) {}
7. Типові помилки під час роботи з незмінними record-класами
Помилка № 1: спроба змінити поле після створення.
Початківці часто намагаються написати p.x = 42; або p.x(42); для record-обʼєкта. Але компілятор одразу скаже: «Так не можна! Поле final, сеттера немає».
Помилка № 2: використання змінюваних обʼєктів як компонентів record.
Якщо ви додали в record поле типу List, Map, масив або інший змінюваний обʼєкт, то сам record не захистить вас від змін вмісту цього обʼєкта. Наприклад, якщо у вас є record User(List<String> hobbies), то хтось може додати або видалити елемент зі списку, і це змінить стан вашого record-обʼєкта. Щоб цього уникнути, використовуйте незмінні колекції (List.copyOf, Collections.unmodifiableList) або робіть копії колекцій усередині конструктора record.
Помилка № 3: хибне розуміння незмінності.
Дехто думає, що якщо обʼєкт — record, то він захищений від будь-яких змін. Насправді якщо поля — посилання на змінювані обʼєкти, їхній вміст можна змінити, і це призведе до неочікуваних багів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ