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.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ