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

Як створити незмінний клас в Java?

Щоб створити незмінний клас, дотримуйтесь цих кроків:

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

Junior Level

Щоб створити незмінний клас, дотримуйтесь цих кроків:

Крок 1: Оголосіть клас як final

public final class Person {

Крок 2: Зробіть усі поля private final

    private final String name;
    private final int age;

Крок 3: Не створюйте сетери

Тільки гетери (методи для читання):

    public String getName() { return name; }
    public int getAge() { return age; }

Крок 4: Ініціалізуйте поля в конструкторі

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

Повний приклад

public final 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; }
}

Коли НЕ створювати незмінний клас

  • JPA-entities — зазвичай змінні (ORM вимагає setters, no-arg constructor)
  • Прості скрипти/прототипи — швидкість розробки важливіша
  • DTO для серіалізації — частіше зручні mutable з setters

Middle Level

Робота зі змінними полями

Якщо клас містить колекції або інші об’єкти, що змінюються, потрібно робити захисні копії:

public final class ImmutableReport {
    private final String title;
    private final List<String> items;

    public ImmutableReport(String title, List<String> items) {
        this.title = title;
        this.items = (items == null) ? List.of() : List.copyOf(items);
    }

    public String getTitle() { return title; }

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

Реалізація через Record (Java 14+)

public record ImmutableReport(String title, List<String> items) {
    public ImmutableReport {
        items = List.copyOf(items); // компактний конструктор
    }
}

record автоматично:

  • Робить клас final
  • Робить поля private final
  • Генерує конструктор, equals(), hashCode(), toString()

П’ять правил Джошуа Блоха

  1. Не надавайте методи-мутатори
  2. Забезпечте неможливість розширення класу (final)
  3. Зробіть усі поля final
  4. Зробіть усі поля private
  5. Забезпечте ексклюзивний доступ до змінних компонентів

Senior Level

Небезпека витоку “this” (Constructor Escape)

public ImmutableClass() {
    ExternalService.register(this); // ПОМИЛКА!
}

Якщо інший потік звернеться до об’єкта до завершення конструктора, він побачить final поля у дефолтному стані.

Глибоке копіювання (Deep Copy)

Якщо список містить змінні об’єкти, List.copyOf недостатньо:

public record Order(Long id, List<Item> items) {
    public Order {
        items = items.stream()
            .map(original -> new Item(original)) // Замість clone() (який вважається broken — Effective Java Item 13): конструктор копіювання — надійніше
            .toList();
    }
}

Reflection атака

Починаючи з Java 9 (Project Jigsaw) і особливо з Java 16+ (JEP 396) доступ через reflection до java.base полів заблоковано за замовчуванням.

Резюме для Senior

  • Звичайного final недостатньо для змінних посилальних типів
  • Завжди використовуйте List.copyOf() або Collections.unmodifiableList()
  • Пам’ятайте про Constructor Escape
  • Для простих DTO обирайте Records
  • Глибока незмінність вимагає копіювання всієї ієрархії об’єктів

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

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

  • 4 кроки: final class, private final поля, немає сетерів, ініціалізація в конструкторі
  • П’ять правил Джошуа Блоха: немає мутаторів, немає розширення, final поля, private поля, захист змінних компонентів
  • Для змінних полів — List.copyOf() в конструкторі та повернення незмінної обгортки з гетера
  • Records (Java 14+) автоматично роблять клас final, поля private final, генерують equals/hashCode/toString
  • Constructor Escape — не можна передавати this назовні під час конструктора
  • Deep Copy: якщо список містить змінні об’єкти, потрібно клонувати кожен елемент
  • Коли НЕ створювати: JPA-entities, прості прототипи, DTO для серіалізації

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

  • Що робити з колекціями в конструкторі?List.copyOf(input) створює незалежну копію
  • Records vs звичайний клас? — Records: мінімум коду, але немає успадкування; звичайний клас: повний контроль
  • Що таке Constructor Escape? — Публікація this до завершення конструктора = інший потік побачить null-поля
  • Чи достатньо List.copyOf для глибокої незмінності? — Ні, якщо елементи списку змінні

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

  • «Просто final полів — і все» — без defensive copy змінні поля вразливі
  • «Можна передати this в конструкторі для реєстрації» — це Constructor Escape
  • «List.copyOf копіює елементи» — копіюється лише контейнер, елементи ті самі
  • «Record вирішує всі проблеми» — колекції в Records теж потрібно копіювати вручну

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

  • [[1. Що таке незмінний (ім’ютабельний) об’єкт]]
  • [[7. Що таке ключове слово final і як воно допомагає у створенні незмінних класів]]
  • [[8. Чи достатньо зробити всі поля final для незмінності]]
  • [[9. Що робити, якщо поле класу посилається на змінний об’єкт]]
  • [[20. Що таке Record і як він допомагає створювати незмінні класи]]