JavaRush /Курсы /JAVA 25 SELF /CopyOnWrite коллекции, unmodifiable wrappers

CopyOnWrite коллекции, unmodifiable wrappers

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.

1
Задача
JAVA 25 SELF, 34 уровень, 2 лекция
Недоступна
Защита списка запрещенных слов 🚫
Защита списка запрещенных слов 🚫
1
Задача
JAVA 25 SELF, 34 уровень, 2 лекция
Недоступна
Журнал событий в реальном времени 📈
Журнал событий в реальном времени 📈
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Андрей Уровень 41
28 мая 2026

Все читающие потоки получают «свою» версию массива, 
которая не меняется, пока они её читают.
Читающие потоки не получают свою копию. Все видят одну и ту же копию и она может меняться, т.к. при изменении выполняется блокировка, а читающие потоки ждут когда блокировку отпустит. Так говорит документация. А тут видимо имеется в виду работа итератора и он таки да итерирует по неизменяемой копии массива, актуальной на момент создания итератора. Тогда если N потоков получат итератор, то у кажого из N итераторов будет своя копия.
nastya_zhadan Уровень 66
30 сентября 2025
Внезапно, многопоточность))