Питання 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 і як він допомагає створювати незмінні класи]]