Вопрос 29 · Раздел 13

Как правильно работать с коллекциями в иммутабельных классах?

Коллекции — самая сложная часть иммутабельных классов. Нужно защитить коллекцию на всех этапах.

Версии по языкам: English Russian Ukrainian

Junior Level

Коллекции — самая сложная часть иммутабельных классов. Нужно защитить коллекцию на всех этапах.

Три правила

1. Копируйте при получении (конструктор)

public final class Group {
    private final List<String> members;

    public Group(List<String> members) {
        this.members = List.copyOf(members); // копия!
    }
}

2. Возвращайте защищённую версию (геттер)

public List<String> getMembers() {
    return members; // List.copyOf уже вернул иммутабельный список
}

3. Не храните прямые ссылки

// ПЛОХО
public Group(List<String> members) {
    this.members = members; // прямая ссылка — можно менять извне
}

Middle Level

Защита при создании

// Java 10+ — лучший вариант
this.list = List.copyOf(input);

// Java 8
this.list = Collections.unmodifiableList(new ArrayList<>(input));

Защита при чтении

  • Если поле уже иммутабельное (List.copyOf) — можно возвращать напрямую
  • Если мутабельное — оборачивайте: Collections.unmodifiableList(this.list)

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

List.copyOf защищает контейнер, но не элементы:

public record Team(String name, List<User> users) {
    public Team {
        users = List.copyOf(users); // контейнер защищён
    }
}
// Но: team.users().get(0).setName("...") — МОЖНО!

Решение — глубокое копирование:

public record Team(String name, List<User> users) {
    public Team {
        users = users.stream()
            .map(u -> new User(u.getName(), u.getRole())) // копия каждого
            .toList();
    }
}

Глубокое копирование необходимо только если элементы коллекции мутабельны. Для String, Integer, Record — достаточно List.copyOf().

Специализированные библиотеки

  • Vavr — персистентные коллекции (structural sharing)
  • GuavaImmutableList, ImmutableMap

Senior Level

Различайте Unmodifiable и Immutable

Характеристика Unmodifiable Wrapper Immutable Copy
Копия данных Нет Да
Связь с оригиналом Живая Разорвана
Скорость создания O(1) O(n)
Безопасность Частичная Полная
List<String> original = new ArrayList<>(List.of("A"));
List<String> unmod = Collections.unmodifiableList(original);
original.set(0, "B"); // unmod тоже изменился! — живая ссылка

List<String> immutable = List.copyOf(original);
original.set(0, "C"); // immutable остался "B" — независимая копия

Производительность копирования

  • List.copyOf() оптимизирован: если вход уже иммутабелен, вернёт тот же объект — экономия памяти и CPU.
  • Для больших коллекций в горячих путях рассмотрите Vavr с $O(\log n)$ через structural sharing
  • Глубокое копирование — $O(n \times m)$, где m = глубина графа

Records и коллекции

public record UserGroup(String name, List<String> members) {
    public UserGroup {
        members = List.copyOf(members); // обязательно в компактном конструкторе
    }
}

Резюме для Senior

  • Всегда разрывайте связь с оригинальной коллекцией через копирование
  • Различайте “немодифицируемую обёртку” и “иммутабельную копию”
  • Коллекции JDK — только Shallow Immutability
  • Для больших данных — персистентные структуры данных (Vavr)
  • Deep Copy обязателен для мутабельных элементов коллекции

🎯 Шпаргалка для интервью

Обязательно знать:

  • Три правила: копировать в конструкторе, возвращать защищённую версию из геттера, не хранить прямые ссылки
  • List.copyOf(input) — лучший вариант (Java 10+), создаёт независимую иммутабельную копию
  • Глубокая иммутабельность: если элементы мутабельны, нужно клонировать каждый через stream.map
  • Unmodifiable vs Immutable: обёртка (O(1), связь) vs копия (O(n), нет связи)
  • Records: обязательно копировать коллекции в компактном конструкторе
  • List.copyOf оптимизирован: если вход уже иммутабелен, вернёт тот же объект

Частые уточняющие вопросы:

  • List.copyOf защищает элементы? — Нет, только контейнер; мутабельные элементы можно менять
  • Когда глубокое копирование НЕ нужно? — Когда элементы сами иммутабельны (String, Integer, Record)
  • UnmodifiableList vs List.copyOf в геттере? — Если поле уже List.copyOf — вернуть напрямую; если mutable — unmodifiableList
  • Vavr vs JDK коллекции? — Vavr: structural sharing O(log n), JDK: полное копирование O(n)

Красные флаги (НЕ говорить):

  • «List.copyOf = полная защита» — это shallow, элементы те же
  • «Можно вернуть оригинал если коллекция private» — caller может иметь ссылку на оригинал
  • «Глубокое копирование всегда нужно» — для String/Integer элементов избыточно
  • «UnmodifiableList в конструкторе — защита» — это обёртка, не копия

Связанные темы:

  • [[9. Что делать, если поле класса ссылается на мутабельный объект]]
  • [[10. Что такое defensive copy (защитная копия)]]
  • [[12. Как защитить коллекцию от изменений]]
  • [[14. В чём разница между shallow copy и deep copy]]
  • [[20. Что такое Record и как он помогает создавать иммутабельные классы]]