JavaRush /Курси /JAVA 25 SELF /Колекції CopyOnWrite, незмінювані обгортки

Колекції CopyOnWrite, незмінювані обгортки

JAVA 25 SELF
Рівень 34 , Лекція 2
Відкрита

1. Unmodifiable wrappers: обгортки над колекціями

Іноді у коді вже є колекція, яку хтось може випадково (або й не зовсім випадково) змінити. Наприклад, у вас є список користувачів, який ви хочете віддати назовні, але не бажаєте, щоб хтось його модифікував:

List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");

Ви повертаєте цей список із методу, а хтось робить users.add("Hacker"); — і ось у системі вже з’явився новий несанкціонований користувач! Як захиститися?

Обгортки з Collections

У Java вже давно існують спеціальні методи-обгортки у класі Collections:

  • Collections.unmodifiableList(list)
  • Collections.unmodifiableSet(set)
  • Collections.unmodifiableMap(map)

Ці методи повертають обгортку над вашою колекцією, яка не дозволяє змінювати колекцію через саму обгортку. Спроба додати, видалити або змінити елемент через обгортку спричинить UnsupportedOperationException.

Приклад:

import java.util.*;

public class Demo {
    public static void main(String[] args) {
        List<String> modifiable = new ArrayList<>();
        modifiable.add("Alice");
        modifiable.add("Bob");

        // Створюємо незмінювану обгортку
        List<String> unmodifiable = Collections.unmodifiableList(modifiable);

        System.out.println(unmodifiable); // [Alice, Bob]

        // Спробуємо додати елемент через обгортку
        try {
            unmodifiable.add("Charlie"); // Отримаємо UnsupportedOperationException.
        } catch (UnsupportedOperationException e) {
            System.out.println("Не можна змінити колекцію: " + e);
        }
    }
}

Важливо!

  • Обгортка НЕ робить початкову колекцію незмінюваною. Якщо хтось має посилання на початкову колекцію, він усе ще може її змінювати.
  • Усі зміни початкової колекції буде видно через обгортку.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]

Тобто, якщо десь у коді хтось додасть або видалить елемент в оригінальному списку, обгортка це побачить. Це не «заморожування», а лише заборона на зміну через саму обгортку.

Обгортки для інших колекцій

Так само можна створювати обгортки для Set, Map і навіть для більш екзотичних структур:

Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);

Порівняння з фабричними методами (List.of та ін.)

  • List.of(...) створює нову справді незмінювану колекцію, до якої від самого початку неможливо нічого додати.
  • Collections.unmodifiableList(list) — це обгортка над наявною колекцією. Якщо початковий список зміниться, зміниться й обгортка.

Таблиця: порівняння підходів

List.of(...)
Collections.unmodifiableList(list)
Можна додати? Ні Ні (через обгортку)
Можна додати до початкової колекції? Не застосовується Так
Видно зміни? Ні Так
Можна додати null? Ні (NPE) Так (якщо початкова колекція дозволяє)
Реалізація Власна Обгортка над наявною колекцією

2. Колекції CopyOnWrite

У багатопотокових програмах часто постає завдання: один потік (або кілька) читає колекцію, а інший (або інші) інколи її змінює. Звичайні колекції тут не підходять: можливі умови гонки, помилки, ConcurrentModificationException та інші проблеми багатопотокового середовища.

Для таких випадків існують колекції CopyOnWrite — вони спеціально призначені для сценаріїв, де читання відбуваються часто, а зміни — рідко.

Як це працює?

  • Під час кожної зміни (додавання, видалення, заміни) колекція створює нову копію внутрішнього масиву.
  • Усі потоки-читачі отримують «свою» версію масиву, яка не змінюється, доки вони її читають.
  • Це робить читання абсолютно безпечним і не потребує синхронізації.

Основні класи

  • CopyOnWriteArrayList<E>
  • CopyOnWriteArraySet<E>

Вони розташовані у пакеті java.util.concurrent.

Приклад використання

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("Alpha");
        cowList.add("Beta");

        // Можна безпечно ітеруватися, навіть якщо хтось паралельно додає елементи
        for (String s : cowList) {
            System.out.println(s);
            cowList.add("Gamma"); // Не спричинить ConcurrentModificationException!
        }

        System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
    }
}

Особливості:

  • Ітератор колекцій CopyOnWrite завжди «бачить» знімок колекції на момент створення ітератора.
  • Якщо після створення ітератора хтось додав елементи, ітератор їх не побачить.
  • Можна безпечно додавати/видаляти елементи під час обходу — жодних ConcurrentModificationException!

Коли використовувати колекції CopyOnWrite?

Вони підходять для ситуацій, де в програмі працює багато потоків, які переважно читають дані з колекції, а операції зміни виконуються дуже рідко. Класичний приклад — список слухачів подій (event listeners): нових слухачів додають або видаляють нечасто, зате сповіщення цих слухачів відбувається постійно.

Приклад — підписники подій

import java.util.concurrent.CopyOnWriteArrayList;

public class EventBus {
    private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();

    public void subscribe(Runnable listener) {
        listeners.add(listener);
    }

    public void publishEvent() {
        for (Runnable listener : listeners) {
            listener.run(); // безпечно, навіть якщо хтось підписався/відписався щойно!
        }
    }
}

Недоліки колекцій CopyOnWrite

  • Повільно для частих змін: кожна зміна — це створення нової копії масиву, що дорого за часом і пам’яттю.
  • Неефективно для великих колекцій: якщо колекція велика, копіювання масиву — витратна операція.

3. Порівняння: коли що використовувати?

Unmodifiable wrappers (Collections.unmodifiable...)

Коли використовувати: коли у вас вже є колекція, яку ви хочете захистити від змін через зовнішній код, але внутрішні зміни (власник колекції) допустимі.

Потокобезпечність: не гарантується! Якщо початкова колекція змінюється з іншого потоку, можливі гонки та помилки.

Фабричні методи (List.of, Set.of, Map.of)

Коли використовувати: коли ви хочете створити сталу незмінювану колекцію одразу, без можливості зміни нізвідки.

Потокобезпечність: гарантується (колекцію взагалі не змінюють).

Колекції CopyOnWrite

Коли використовувати: у багатопотокових сценаріях, де багато читань і мало записів. Наприклад, для списків підписників.

Потокобезпечність: так, повністю потокобезпечні.

Незмінюваність: ні, колекцію можна змінювати, але щоразу створюється нова копія, щоб читачі не постраждали.

4. Типові помилки та особливості реалізації

Помилка № 1: Очікування «заморожування» початкової колекції через обгортку. Багато хто вважає, що Collections.unmodifiableList(list) робить колекцію повністю незмінюваною. Насправді, якщо в когось залишилося посилання на оригінальний список, він може його змінювати, і ці зміни буде видно через обгортку. Рішення: якщо потрібна справжня незмінюваність — використовуйте List.copyOf(list) (Java 10+) або List.of(...).

Помилка № 2: Використання CopyOnWrite для часто змінюваної колекції. Якщо в CopyOnWriteArrayList постійно додають або видаляють елементи, це призведе до проблем із продуктивністю та пам’яттю. CopyOnWrite підходить лише для сценаріїв «багато читань, мало записів».

Помилка № 3: Очікування, що обгортки — потокобезпечні. Collections.unmodifiableList не робить колекцію потокобезпечною! Якщо початковий список змінюється з різних потоків, можливі помилки.

Помилка № 4: Використання колекцій із List.of або Set.of з null. На відміну від звичайних колекцій, фабричні методи не допускають null — спроба додати або навіть створити колекцію з null призведе до NullPointerException.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ