JavaRush /Курси /JAVA 25 SELF /Immutability — незмінність record-класів

Immutability — незмінність record-класів

JAVA 25 SELF
Рівень 22 , Лекція 1
Відкрита

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, то він захищений від будь-яких змін. Насправді якщо поля — посилання на змінювані обʼєкти, їхній вміст можна змінити, і це призведе до неочікуваних багів.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ