📦 Розділ 4 · Питання #23

Що таке Collections.unmodifiableList()

Метод Collections.unmodifiableList(List 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. Витоки пам’яті та проблеми конкурентності

  1. Неочікуваний ConcurrentModificationException у багатопотоковості: Якщо Потік 1 виконує читання for (String s : unmodifiableList), а Потік 2 в цей самий момент викликає originalList.add("X"), Потік 1 отримає ConcurrentModificationException. Обгортка не забезпечує жодної потокобезпеки, оскільки лічильник модифікацій modCount вихідного списку змінився!
  2. Прихований витік пам’яті (Memory Retention): Якщо довгоживучий сервіс зберігає посилання на unmodifiableList, він тим самим утримує в купі (Heap) посилання на весь початковий об’єкт списку та всі його елементи, навіть якщо клієнту потрібна була лише невелика частина даних.
  3. Оптимізація в 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)?

Відповідь:

  1. Якщо зловмисник або небезпечний плагін має доступ до рефлексії, він може прочитати приватне поле list всередині обгортки й отримати прямий доступ до вихідного мутабельного списку.
  2. Якщо сам сервіс продовжує модифікувати internalList з інших потоків, клієнт може зіткнутися зі станом гонитви та аварійними ConcurrentModificationException. Для надійної інкапсуляції слід повертати ізольовану копію через List.copyOf() або new ArrayList<>(internalList).

Типові помилки та Red Flags

  • ❌ Стверджувати, що unmodifiableList копіює дані в пам’яті: Це лише легкозважене представлення ($O(1)$ View) над вихідною колекцією.
  • ❌ Впевненість у тому, що елементи списку стають незмінними: Захищається тільки структура списку, а не поля об’єктів усередині нього.
  • ❌ Вважати unmodifiableList потокобезпечним: Модифікація вихідного списку паралельним потоком викличе ConcurrentModificationException під час ітерації обгорткою.
  • ❌ Передача null у List.copyOf(): На відміну від unmodifiableList, List.copyOf суворо забороняє null як для самої колекції, так і серед її елементів.

Пов’язані теми