Вопрос 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 и как он помогает создавать иммутабельные классы]]