Що таке Collections.unmodifiableList()
Метод Collections.unmodifiableList(List extends T> list) повертає обгортку над переданим списком, яка забороняє будь-які прямі зміни його структури.
🟢 Junior Level
1. Призначення та базова поведінка
Метод Collections.unmodifiableList(List<? extends T> list) повертає обгортку над переданим списком, яка забороняє будь-які прямі зміни його структури.
При спробі викликати будь-який метод модифікації (add, remove, set, clear) викидається виняток UnsupportedOperationException:
List<String> original = new ArrayList<>();
original.add("Java");
original.add("Kotlin");
List<String> unmodifiable = Collections.unmodifiableList(original);
// Операції читання дозволені:
System.out.println(unmodifiable.get(0)); // "Java"
// Операції зміни заборонені:
unmodifiable.add("Scala"); // Викине java.lang.UnsupportedOperationException!
unmodifiable.remove(0); // Викине java.lang.UnsupportedOperationException!
2. Принциповий факт: це View (представлення), а не копія
[!WARNING]
Collections.unmodifiableList()не копіює елементи у новий масив. Він створює легкозважену проекцію (View) поверх вихідного списку за час $O(1)$. Якщо вихідний список буде змінено напряму, ці зміни миттєво стануть помітними через «незмінну» обгортку!
original.add("Scala"); // Модифікуємо оригінал в обхід обгортки
// Зміна одразу ж відображається в unmodifiable-обгортці:
System.out.println(unmodifiable.size()); // 3!
System.out.println(unmodifiable.get(2)); // "Scala"
🟡 Middle Level
1. Поверхнева незмінність (Shallow Immutability)
unmodifiableList захищає виключно структуру самого списку (не можна додати, видалити чи замінити посилання на елементи), але не захищає внутрішній стан самих об’єктів, які знаходяться всередині списку:
class User {
private String name;
public User(String name) { this.name = name; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
List<User> list = new ArrayList<>();
list.add(new User("Олексій"));
List<User> unmodUsers = Collections.unmodifiableList(list);
// Мутація внутрішнього стану об'єкта ДОЗВОЛЕНА:
unmodUsers.get(0).setName("Зловмисник"); // Спрацює без помилок!
System.out.println(unmodUsers.get(0).getName()); // "Зловмисник"
Щоб досягти справжньої глибокої незмінності (Deep Immutability), самі елементи списку мають бути спроєктовані як незмінні сутності (Immutable Objects / Records).
2. Порівняння: unmodifiableList проти List.copyOf та List.of
З появою Java 9 та Java 10 з’явилися сучасні інструменти створення по-справжньому незмінних колекцій:
| Характеристика | Collections.unmodifiableList(list) |
List.copyOf(collection) (Java 10+) |
List.of(e1, e2...) (Java 9+) |
|---|---|---|---|
| Тип структури | Незмінне представлення (View) | Справжня незмінна копія (Copy) | Справжній незмінний список |
| Зв’язок з оригіналом | ✅ Є (зміни оригіналу видно) | ❌ Немає (повна ізоляція) | Оригіналу немає |
| Складність створення | $O(1)$ (без копіювання даних) | $O(N)$ (копіює масив) | $O(N)$ / $O(1)$ |
Підтримка null |
Залежить від базового списку | ❌ Заборонено (NullPointerException) |
❌ Заборонено (NPE) |
| Основне застосування | Повернення View без алокацій |
Захисне копіювання (Defensive Copy) | Фіксовані константи |
🔴 Senior Level
1. Збереження маркерного інтерфейсу RandomAccess
Ефективність роботи багатьох алгоритмів стандартної бібліотеки (наприклад, Collections.binarySearch()) безпосередньо залежить від наявності інтерфейсу-маркера RandomAccess:
- Для
RandomAccessсписків (наприклад,ArrayList) алгоритм використовує прямий доступ за індексом за $O(1)$. - Для списків без
RandomAccess(наприклад,LinkedList) алгоритм перемикається на обхід через ітератори, щоб не деградувати до $O(N^2)$.
Розробники JDK врахували це в реалізації Collections.unmodifiableList():
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
Якщо вихідний список реалізує RandomAccess, обгортка гарантовано зберігає цю алгоритмічну оптимізацію.
2. Витоки пам’яті та проблеми конкурентності
- Неочікуваний
ConcurrentModificationExceptionу багатопотоковості: Якщо Потік 1 виконує читанняfor (String s : unmodifiableList), а Потік 2 в цей самий момент викликаєoriginalList.add("X"), Потік 1 отримаєConcurrentModificationException. Обгортка не забезпечує жодної потокобезпеки, оскільки лічильник модифікаційmodCountвихідного списку змінився! - Прихований витік пам’яті (Memory Retention):
Якщо довгоживучий сервіс зберігає посилання на
unmodifiableList, він тим самим утримує в купі (Heap) посилання на весь початковий об’єкт списку та всі його елементи, навіть якщо клієнту потрібна була лише невелика частина даних. - Оптимізація в
List.copyOf(): Якщо передати вList.copyOf(collection)колекцію, яка вже є внутрішньою незмінною реалізацією (створеною черезList.of()абоList.copyOf()), метод не виконує копіювання, а повертає те саме посилання за $O(1)$:List<String> l1 = List.of("A", "B"); List<String> l2 = List.copyOf(l1); System.out.println(l1 == l2); // true!
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«
Collections.unmodifiableList()повертає декоратор списку, який блокує будь-які спроби структурної модифікації (add,remove,set) викиданнямUnsupportedOperationException. Принципово важливо розуміти, що це View (представлення), а не ізольована копія: створення виконується за $O(1)$ часу й пам’яті, проте будь-які зміни у вихідному списку миттєво транслюються через обгортку. Окрім того, обгортка забезпечує лише поверхневу незмінність — поля внутрішніх об’єктів залишаються мутабельними. У сучасній Java для створення повністю ізольованих незмінних копій використовуєтьсяList.copyOf()».
3 каверзних запитання з інтерв’ю та відповіді
1. У чому різниця між поняттями «Unmodifiable Collection» та «Immutable Collection» в Java?
Відповідь:
- Unmodifiable (Незмінна для модифікації): колекцію не можна змінити через прямий виклик методів обгортки (
unmodifiableList), але її стан може змінитися внаслідок модифікації базової колекції, на яку оформлено проєкцію. - Immutable (Абсолютно незмінна): стан колекції та посилання на елементи гарантовано не можуть змінитися за жодних обставин у всій JVM (як у
List.of()абоList.copyOf()).
2. Що станеться, якщо викликати unmodifiableList.iterator().remove()?
Відповідь: Виклик завершиться винятком UnsupportedOperationException. Метод iterator() обгортки повертає спеціальний внутрішній декоратор UnmodifiableIterator, у якого методи remove(), add() та set() перевизначені й безумовно викидають UnsupportedOperationException.
3. Чому в захищеному коді (Security-Critical API) небезпечно повертати Collections.unmodifiableList(internalList)?
Відповідь:
- Якщо зловмисник або небезпечний плагін має доступ до рефлексії, він може прочитати приватне поле
listвсередині обгортки й отримати прямий доступ до вихідного мутабельного списку. - Якщо сам сервіс продовжує модифікувати
internalListз інших потоків, клієнт може зіткнутися зі станом гонитви та аварійнимиConcurrentModificationException. Для надійної інкапсуляції слід повертати ізольовану копію черезList.copyOf()абоnew ArrayList<>(internalList).
Типові помилки та Red Flags
- ❌ Стверджувати, що
unmodifiableListкопіює дані в пам’яті: Це лише легкозважене представлення ($O(1)$ View) над вихідною колекцією. - ❌ Впевненість у тому, що елементи списку стають незмінними: Захищається тільки структура списку, а не поля об’єктів усередині нього.
- ❌ Вважати
unmodifiableListпотокобезпечним: Модифікація вихідного списку паралельним потоком викличеConcurrentModificationExceptionпід час ітерації обгорткою. - ❌ Передача
nullуList.copyOf(): На відміну відunmodifiableList,List.copyOfсуворо забороняєnullяк для самої колекції, так і серед її елементів.