Як правильно працювати з колекціями в незмінних класах?
Колекції — найскладніша частина незмінних класів. Потрібно захистити колекцію на всіх етапах.
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 і як він допомагає створювати незмінні класи]]