Каковы ключевые правила создания иммутабельного (immutable) класса в Java?

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

Ответ

Чтобы класс был полностью иммутабельным, необходимо соблюдать следующие правила:

  1. Объявите класс как final. Это предотвращает создание подклассов, которые могли бы изменить поведение или состояние.
  2. Объявите все поля как private final. Поля инициализируются только один раз — в конструкторе.
  3. Не предоставляйте сеттеров (setter methods). Отсутствие методов, меняющих состояние, — основа иммутабельности.
  4. Защищайте изменяемые поля. Если класс содержит ссылки на изменяемые объекты (например, List, Map, массивы):
    • В конструкторе: Не сохраняйте переданную извне ссылку. Создавайте и сохраняйте глубокую копию (deep copy) или используйте защитную копию (defensive copy).
    • В геттерах: Не возвращайте прямую ссылку на внутреннее поле. Возвращайте копию поля или неизменяемое представление (например, через Collections.unmodifiableList()).

Пример корректного иммутабельного класса:

import java.util.*;

public final class ImmutablePerson {
    private final String name;
    private final List<String> hobbies; // Изменяемый объект!

    // Конструктор с защитным копированием
    public ImmutablePerson(String name, List<String> hobbies) {
        this.name = name;
        // Создаем новый ArrayList из переданного списка
        this.hobbies = new ArrayList<>(hobbies);
    }

    public String getName() {
        return name;
    }

    // Геттер, возвращающий неизменяемое представление
    public List<String> getHobbies() {
        return Collections.unmodifiableList(hobbies);
    }
}
// Использование
List<String> originalHobbies = new ArrayList<>(Arrays.asList("Reading", "Cycling"));
ImmutablePerson person = new ImmutablePerson("Alice", originalHobbies);
originalHobbies.add("Gaming"); // Не влияет на состояние person
person.getHobbies().add("Swimming"); // Выбросит UnsupportedOperationException