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) — это обёртка над существующей коллекцией. Если исходный список изменится, изменится и обёртка.
Таблица: сравнение подходов
|
|
|
|---|---|---|
| Можно добавить? | Нет | Нет (через обёртку) |
| Можно добавить в исходную? | Не применимо | Да |
| Видны изменения? | Нет | Да |
| Можно положить 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ