Как реализовать иммутабельный (неизменяемый) класс в 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).
  • Геттер: Возвращая неизменяемое представление, мы предотвращаем модификацию через полученную ссылку.

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

Ответ 18+ 🔞

А, ну это же классика, блядь! Сейчас объясню на пальцах, как сделать этот ваш иммутабельный класс, чтобы он был крепче, чем совесть у чиновника. Сиди, слушай.

Значит, смотри. Есть у тебя класс, и ты хочешь, чтобы он был как скала — чтобы никакой внешний код не мог его пошатнуть, изменить его внутренности. Особенно если там внутри какая-нибудь коллекция сидит, типа списка телефонов. Это же лазейка, блядь! Дали тыщу раз на это ебало!

Вот основные правила, без которых нихуя не получится:

  1. Класс в final. Чтобы какой-нибудь умник не наследовался и не накостылял тебе бяку в наследничке. Нахуй наследников, мы тут сами с усами.
  2. Поля все private final. Private — чтобы с улицы не дотянулись, final — чтобы один раз присвоили и забыли, как страшный сон.
  3. Никаких сеттеров! Вообще, нихуя. Забыл это слово. Если захочешь "изменить" объект — создавай новый, с новыми данными. Это и есть иммутабельность, ёпта.
  4. А теперь самое важное, блядь — защитные копии! Это как презерватив для твоего класса. Надеваешь в двух местах:
    • В конструкторе: Когда тебе передают коллекцию извне, ты не должен тупо сохранять ссылку на неё. А то придут эти пидарасы, которые тебе коллекцию передали, и потом её нахуй изменят, а твой объект, как лох, будет смотреть на эти изменения. Надо создать НОВУЮ коллекцию и скопировать в неё все элементы. new ArrayList<>(входящаяКоллекция). Вот так, в рот меня чих-пых!
    • В геттере: Когда ты отдаёшь свою коллекцию наружу, нельзя отдавать прямую ссылку на внутренний список. Отдашь — и опять придут, начнут там add/remove делать. Надо либо новую копию вернуть (return new ArrayList<>(internalList)), либо, что чаще и правильнее, — неизменяемое представление. return Collections.unmodifiableList(internalList). Попробуй изменить такую штуку — получишь UnsupportedOperationException прямо в ебальник.

Вот, смотри, как это выглядит в коде, сука:

import java.util.*;

public final class ImmutablePerson { // Правило 1: final класс
    private final String name; // Правило 2: private final
    private final List<String> phoneNumbers; // Наше поле-коллекция

    // КОНСТРУКТОР. Надеваем первый презерватив.
    public ImmutablePerson(String name, Collection<String> phoneNumbers) {
        this.name = name;
        // ВАЖНО! Не this.phoneNumbers = phoneNumbers; // ПИЗДЕЦ ТАК ДЕЛАТЬ!
        // А вот так:
        // 1. Создаём копию переданного (new ArrayList<>).
        // 2. И сразу оборачиваем в бронежилет (unmodifiableList).
        this.phoneNumbers = Collections.unmodifiableList(
                                new ArrayList<>(phoneNumbers) // Защитное копирование!
                            );
    }

    public String getName() {
        return name; // Со строкой всё просто, она и так иммутабельна
    }

    // ГЕТТЕР. Надеваем второй презерватив.
    public List<String> getPhoneNumbers() {
        // Возвращаем наше неизменяемое представление.
        // Те, кто получит эту ссылку, смогут только смотреть.
        return phoneNumbers; // Это уже unmodifiableList, можно так.
        // Или return new ArrayList<>(phoneNumbers); // Альтернатива — вернуть копию.
    }
}

И последнее предупреждение, блядь, самое важное! Всё это работает на ура, только если элементы внутри коллекции сами по себе иммутабельны (как строки String в нашем примере). Если бы там были какие-нибудь свои объекты, которые можно менять, то хоть оберни коллекцию в титановый ящик — эти объекты изнутри всё равно можно будет изменить. Это называется глубокая неизменяемость, и её надо продумывать отдельно, либо копируя и сами объекты.

Короче, запомни: создавая иммутабельный класс с коллекцией, ты как сапёр — дважды проверяешь, откуда данные пришли и куда уходят. И тогда всё будет пиздато.