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

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

Ответ

Создание иммутабельного класса с полем-коллекцией требует защитных копий (defensive copying) на этапах инициализации и возврата значения, чтобы внешний код не мог изменить внутреннее состояние объекта.

Ключевые шаги:

  1. Объявите класс как final, чтобы запретить наследование.
  2. Объявите все поля как private final.
  3. Не предоставляйте сеттеров (mutators).
  4. В конструкторе создавайте глубокую защитную копию переданной коллекции.
  5. В геттере возвращайте неизменяемое представление (unmodifiable view) или копию коллекции.

Пример реализации:

import java.util.*;

public final class ImmutablePerson {
    private final String name;
    private final List<String> phoneNumbers; // Поле-коллекция

    // Конструктор с защитным копированием
    public ImmutablePerson(String name, Collection<String> phoneNumbers) {
        this.name = name;
        // 1. Создаем новый список на основе переданной коллекции
        // 2. Оборачиваем его в unmodifiableList
        this.phoneNumbers = Collections.unmodifiableList(
                                new ArrayList<>(phoneNumbers)
                            );
    }

    public String getName() {
        return name;
    }

    // Геттер возвращает неизменяемое представление
    public List<String> getPhoneNumbers() {
        // Возвращаем само unmodifiableList поле.
        // Альтернатива: вернуть новую копию -> return new ArrayList<>(phoneNumbers);
        return phoneNumbers;
    }
}

Почему это работает и важно:

  • Конструктор: new ArrayList<>(phoneNumbers) создает копию. Даже если переданная коллекция изменится после создания объекта, внутреннее состояние ImmutablePerson останется прежним.
  • Поле: Collections.unmodifiableList(...) гарантирует, что даже ссылка на внутренний список не позволит его модифицировать (при попытке выбросит UnsupportedOperationException).
  • Геттер: Возвращая неизменяемое представление, мы предотвращаем модификацию через полученную ссылку.

Для модифицируемых объектов внутри коллекции требуется глубокая неизменяемость — элементы коллекции также должны быть иммутабельными, либо при копировании нужно создавать копии и этих объектов.