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 лекция
Недоступна
Журнал событий в реальном времени 📈
Журнал событий в реальном времени 📈
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
nastya_zhadan Уровень 66
30 сентября 2025
Внезапно, многопоточность))