Как правильно работать с коллекциями в иммутабельных классах?
Коллекции — самая сложная часть иммутабельных классов. Нужно защитить коллекцию на всех этапах.
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)
- Guava —
ImmutableList,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 и как он помогает создавать иммутабельные классы]]