Как создать неизменяемый (immutable) класс в Java?

«Как создать неизменяемый (immutable) класс в Java?» — вопрос из категории Java Core, который задают на 23% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Неизменяемый класс — это класс, состояние которого нельзя изменить после создания. Это ключевая концепция для написания потокобезопасного и предсказуемого кода.

Строгие правила создания immutable-класса:

  1. Объявите класс как final — чтобы от него нельзя было наследоваться и переопределить методы.
  2. Все поля объявите как private и final — инициализация только в конструкторе.
  3. Не предоставляйте сеттеров (setter methods).
  4. Инициализируйте все поля через конструктор, выполняя глубокое копирование для mutable-аргументов.
  5. Для getter-методов, возвращающих mutable-объекты, возвращайте защищённые копии или неизменяемые представления.

Пример правильного immutable-класса:

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

public final class ImmutablePerson {
    // Примитивы и String уже immutable
    private final String name;
    private final int age;

    // Mutable-объект! Требует особой обработки.
    private final List<String> hobbies;

    // Конструктор с защитным копированием (defensive copy)
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // 1. Создаём копию переданного списка
        this.hobbies = new ArrayList<>(hobbies);
        // 2. Опционально: делаем список read-only
        // this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
    }

    // Getter для immutable-полей
    public String getName() { return name; }
    public int getAge() { return age; }

    // Getter для mutable-поля: ВОЗВРАЩАЕМ КОПИЮ или unmodifiable view
    public List<String> getHobbies() {
        // Возвращаем новую копию, чтобы исходный список нельзя было изменить извне
        return new ArrayList<>(hobbies);
        // Или возвращаем неизменяемое представление (предпочтительнее для производительности)
        // return Collections.unmodifiableList(hobbies);
    }
}

Почему это работает:

  • Поля final гарантируют, что ссылка не изменится после конструктора.
  • Защитное копирование в конструкторе и геттерах изолирует внутреннее состояние класса от внешнего кода.
  • Отсутствие сеттеров исключает прямой способ изменения.

Для ещё большей надёжности используйте библиотеки, такие как AutoValue или Immutables, которые генерируют корректный immutable-код.