Питання 1 · Розділ 13

Що таке незмінний (ім'ютабельний) об'єкт?

Це означає, що об'єкт створюється один раз і назавжди залишається в тому самому стані — як фотографія, яку не можна відредагувати.

Мовні версії: English Russian Ukrainian

Junior Level

Незмінний (ім’ютабельний) об’єкт — це об’єкт, стан якого не змінюється після створення. Усі поля встановлюються один раз у конструкторі, і після цього об’єкт не надає способів змінити свої дані.

Це означає, що об’єкт створюється один раз і назавжди залишається в тому самому стані — як фотографія, яку не можна відредагувати.

Простий приклад

public final 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; }
    // Немає сетерів — стан не можна змінити
}

Ключові ознаки

  • Немає сетерів (setX, setY тощо)
  • Усі поля — private final
  • Клас — final (не можна успадковуватись)

Коли НЕ використовувати незмінні об’єкти

  1. High-allocation hot-path — мільйони змін/сек, GC pressure
  2. Масивні структури даних — копіювання дорожче за мутацію
  3. Прототипи та скрипти — швидкість розробки важливіша за надійність

Незмінний vs final поля vs без сетерів

  • Без сетерів ≠ незмінний: можна змінювати через повернуті посилання на змінні поля
  • Final поля ≠ незмінний: якщо поле посилається на змінний об’єкт, його можна змінити
  • Незмінний: жоден видимий стан не змінюється після конструювання

Middle Level

П’ять правил створення незмінного класу

  1. Клас повинен бути final — підкласи не зможуть додати змінний стан
  2. Усі поля private final — доступ лише через гетери
  3. Немає методів-мутаторів — жодних setX(), add(), remove()
  4. Захист змінних полів — якщо поле посилається на об’єкт, що змінюється (наприклад, List або Date), потрібно робити захисні копії
  5. Безпечна публікація — не передавати this назовні під час конструктора

Робота з колекціями

public final class UserProfile {
    private final String username;
    private final List<String> roles;

    public UserProfile(String username, List<String> roles) {
        this.username = username;
        this.roles = List.copyOf(roles); // захисна копія
        // List.copyOf повертає новий незалежний список.
        // Якщо передано null — буде NPE, тому додайте null-check.
    }

    public List<String> getRoles() {
        return roles; // List.copyOf вже повернув незмінний список
    }
}

Сучасний підхід — Records (Java 14+)

public record UserProfile(String username, List<String> roles) {
    public UserProfile {
        roles = List.copyOf(roles);
    }
}

record автоматично робить клас final, поля private final, генерує конструктор, equals(), hashCode(), toString().


Senior Level

Гарантії Java Memory Model (JMM)

Використання final полів забезпечує безпечну публікацію (safe publication). Згідно зі специфікацією JMM, після завершення конструктора незмінного об’єкта будь-який потік, що отримав посилання на цей об’єкт, гарантовано побачить коректні значення його final полів. Це позбавляє від необхідності використовувати volatile або синхронізацію.

В кінці конструктора відбувається “заморозка” (freeze) усіх final полів — створюється memory barrier, що запобігає переупорядкуванню інструкцій.

Продуктивність та Garbage Collection

Побутує думка, що незмінність шкодить продуктивності. Але в HotSpot JVM (основна реалізація OpenJDK): алокація в Young Generation надзвичайно швидка (Bump-the-pointer — просто зсув вказівника). Інші JVM (Zing, GraalVM) можуть відрізнятися:

  • Нетривалі об’єкти очищуються безкоштовно при Minor GC
  • Відсутність Write Barriers (немає модифікації Old Generation)

Небезпека Constructor Escape

public ImmutableClass() {
    ExternalService.register(this); // ПОМИЛКА! Об'єкт опубліковано до завершення ініціалізації
}

Інший потік може побачити final поля у дефолтному стані (null або 0).

Deep Copy для вкладених змінних об’єктів

Якщо список містить не String, а змінні User, List.copyOf недостатньо — потрібно клонувати кожен елемент.

Резюме для Senior

  • Незмінність — це “контракт безпеки”
  • final на рівні класу та полів — обов’язкова вимога JMM
  • Завжди робіть копії змінних компонентів
  • Використовуйте Records для скорочення бойлерплейту

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Незмінний об’єкт — стан не змінюється після створення
  • Три ключові ознаки: final клас, private final поля, відсутність сетерів
  • П’ять правил: final class, private final поля, немає мутаторів, захист змінних полів, безпечна публікація
  • List.copyOf() / Collections.unmodifiableList() для захисту колекцій
  • Records (Java 14+) автоматично генерують незмінний клас
  • JMM гарантує safe publication для final полів після конструктора
  • Незмінність ≠ thread-safe для посилань на змінні об’єкти
  • Constructor Escape — критична помилка при публікації this до завершення конструктора

Часті уточнюючі запитання:

  • Чи достатньо final полів? — Ні, потрібен ще захист змінних полів та final клас
  • Що таке deep copy? — Рекурсивне копіювання усіх вкладених змінних об’єктів
  • Коли НЕ використовувати незмінність? — High-allocation hot-path, масивні структури, прототипи
  • Records vs звичайний клас? — Records: мінімум boilerplate, але немає успадкування та extra-полів

Червоні прапорці (НЕ говорити):

  • «Незмінний = просто без сетерів» — змінні поля все ще можна змінювати через гетери
  • «Final полів достатньо» — без defensive copy змінні об’єкти вразливі
  • «Незмінність завжди повільніша» — у багатопотоковому середовищі без блокувань вона швидша
  • «Можна змінювати незмінний об’єкт через рефлексію» — в Java 17+ це заблоковано

Пов’язані теми:

  • [[2. Які переваги дає використання незмінних об’єктів]]
  • [[3. Як створити незмінний клас в Java]]
  • [[7. Що таке ключове слово final і як воно допомагає у створенні незмінних класів]]
  • [[8. Чи достатньо зробити всі поля final для незмінності]]
  • [[20. Що таке Record і як він допомагає створювати незмінні класи]]