🔒 Розділ 13 · Питання #13

Що таке Collections.unmodifiableList() і як це працює

Collections.unmodifiableList() не виділяє пам'ять під новий масив елементів і не копіює дані:


🟢 Junior Level

30-секундна відповідь

Collections.unmodifiableList() — це статичний фабричний метод стандартної бібліотеки Java (існує ще з Java 1.2), який обгортає наявний список у спеціальне немодифіковне представлення (Read-Only View).

  • Будь-яка спроба змінити колекцію через це представлення (виклики методів add(), remove(), clear(), set(), replaceAll(), sort()) негайно завершується винятком UnsupportedOperationException.
  • Методи читання (get(), size(), contains(), iterator()) працюють у штатному режимі, прозоро делегуючи виклики оригінальному списку.

Критична пастка: Це ОБГОРТКА, а НЕ копія!

Collections.unmodifiableList() не виділяє пам’ять під новий масив елементів і не копіює дані:

  • Він лише зберігає пряме посилання на вихідний список.
  • Якщо оригінальний список буде змінено в іншому місці програми, усі ці зміни миттєво стануть видимими через unmodifiable-обгортку!
List<String> original = new ArrayList<>();
original.add("Java");

List<String> view = Collections.unmodifiableList(original);

// Спроба прямої модифікації через обгортку:
view.add("Kotlin"); // ❌ ВИНЯТОК: UnsupportedOperationException!

// Модифікація оригінального списку:
original.add("Kotlin"); // ✅ Успішно!

// Зміна негайно відобразилася в "незмінній" обгортці:
System.out.println(view); // Надрукує: [Java, Kotlin]

Наочна аналогія з життя

Уявіть музейну вітрину з броньованим склом:

  • Відвідувач музею дивиться на експонат крізь скло (unmodifiableList) і не може до нього доторкнутися чи забрати.
  • Проте співробітник музею з ключем зі зворотного боку (original) може в будь-який момент прибрати експонат або замінити його на інший.

🟡 Middle Level

Патерн проєктування Decorator (Декоратор)

Всередині метод Collections.unmodifiableList() повертає екземпляр приватного статичного класу Collections.UnmodifiableList:

static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
    final List<? extends E> list; // Посилання на оригінальний список

    UnmodifiableList(List<? extends E> list) {
        super(list);
        this.list = list;
    }

    // Делегування методів читання:
    public E get(int index) { return list.get(index); }
    public int size() { return list.size(); }
    public boolean contains(Object o) { return list.contains(o); }

    // Перехоплення та блокування всіх методів-мутаторів:
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
    public void clear() {
        throw new UnsupportedOperationException();
    }
}

Обгортання ітераторів

Методи iterator() та listIterator() повертають спеціальні реалізації ітераторів, які делегують методи hasNext() та next() вихідному ітератору, але блокують метод remove():

public Iterator<E> iterator() {
    return new Iterator<E>() {
        private final Iterator<? extends E> i = c.iterator();
        public boolean hasNext() { return i.hasNext(); }
        public E next()          { return i.next(); }
        public void remove() {
            throw new UnsupportedOperationException(); // Блокування!
        }
    };
}

Маркерний інтерфейс RandomAccess

Якщо переданий список реалізує інтерфейс швидкої індексної адресації RandomAccess (наприклад, ArrayList), метод Collections.unmodifiableList() повертає спеціалізований підклас UnmodifiableRandomAccessList:

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ?
            new UnmodifiableRandomAccessList<>(list) :
            new UnmodifiableList<>(list));
}

Це зберігає $O(1)$ продуктивність алгоритмів (таких як Collections.binarySearch), що перевіряють умову if (list instanceof RandomAccess).


🔴 Senior Level

Багатопотокові ризики: Ілюзія потокобезпечності

Серед розробників поширена небезпечна омана: «Якщо список немодифіковний, його можна безпечно віддавати на читання в паралельні потоки». Це не так!

  1. ConcurrentModificationException при паралельній ітерації:
    • Якщо Потік 1 ітерується колекцією через unmodifiableList, а Потік 2 в цей час викликає originalList.add(), лічильник модифікацій modCount базового ArrayList збільшується.
    • Ітератор Потоку 1 миттєво падає з ConcurrentModificationException прямо під час виклику next().
  2. Відсутність бар’єрів пам’яті (Safe Publication):
    • Клас UnmodifiableList не використовує ключові слова volatile чи бар’єри пам’яті для елементів вихідного списку.
    • Потік-читач може побачити елементи в стані розірваного читання або зі застарілими закешованими даними в кешах L1/L2 процесора.

Серіалізація та десеріалізація

Клас UnmodifiableList реалізує інтерфейс java.io.Serializable:

  • При серіалізації записується сам об’єкт-обгортка та посилання на оригінальний список.
  • При десеріалізації відновлюється як оригінальний список, так і UnmodifiableList, що його загортає.
  • Важливо: якщо вихідний список не реалізує Serializable, спроба серіалізувати unmodifiableList завершиться винятком NotSerializableException!

Контракт equals() та hashCode()

Цікава архітектурна деталь JDK:

  • Базовий клас UnmodifiableCollection навмисно не перевизначає equals() та hashCode(), успадковуючи їх від Object (порівняння за посиланням ==). Це зроблено тому, що контракти рівності для List та Set є кардинально різними (для List важливий порядок елементів, для Set — лише їх наявність).
  • Підклас UnmodifiableList явно перевизначає equals() та hashCode(), суворо делегуючи їх оригінальному списку list.equals(o) та list.hashCode(), повністю дотримуючись контракту інтерфейсу java.util.List.

4 Tricky Questions

1. Чому Collections.unmodifiableCollection() навмисно НЕ перевизначає equals() та hashCode(), а Collections.unmodifiableList() — перевизначає?

Відповідь: Інтерфейс java.util.Collection свідомо не фіксує контракт для equals() та hashCode(), залишаючи його відкритим, оскільки підтипи List та Set мають взаємовиключні правила:

  • List.equals() вимагає точного збігу елементів та їхніх порядкових позицій.
  • Set.equals() вимагає збігу елементів незалежно від порядку обходу. Якби UnmodifiableCollection реалізував одне з цих правил, це зламало б поліморфізм для іншого підтипу. Тому UnmodifiableCollection залишає реалізацію Object, а спеціалізовані класи UnmodifiableList та UnmodifiableSet реалізують відповідний інтерфейсний контракт, делегуючи виклики внутрішньому списку або сету.

2. Чи скомпілюється виклик Collections.sort(unmodifiableList), і що станеться під час виконання? А виклик unmodifiableList.sort(null)?

Відповідь: Обидва виклики успішно скомпілюються, оскільки метод Collections.sort(List<T>) приймає будь-який об’єкт, що реалізує інтерфейс List. Однак під час виконання:

  • unmodifiableList.sort(...): клас UnmodifiableList явно перевизначає метод за замовчуванням sort(Comparator) інтерфейсу List (який з’явився в Java 8) і викидає new UnsupportedOperationException().
  • Collections.sort(unmodifiableList) всередині себе викликає метод list.sort(), який потрапляє на ту саму реалізацію і негайно викидає UnsupportedOperationException.

3. Чим поведінка UnmodifiableRandomAccessList відрізняється від UnmodifiableList, і чому це критично для продуктивності?

Відповідь: Єдина відмінність UnmodifiableRandomAccessList від UnmodifiableList — наявність порожнього маркерного інтерфейсу implements RandomAccess. Проте це критично для швидкодії стандартних алгоритмів JDK:

  • Такі методи, як Collections.binarySearch(), перевіряють:
    if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD)
        return indexedBinarySearch(list, key); // O(log N) прямого індексного доступу
    else
        return iteratorBinarySearch(list, key); // O(N) послідовного доступу через ітератор
    
  • Якби обгортка втрачала маркер RandomAccess, бінарний пошук по загорнутому ArrayList почав би працювати за $O(N)$ замість $O(\log N)$, спричиняючи колосальні просідання швидкодії.

4. В якому єдиному сценарії використання Collections.unmodifiableList() є кращим, ніж List.copyOf() у сучасній Java?

Відповідь: Collections.unmodifiableList() є кращим у двох випадках:

  1. Екстремальна економія пам’яті та часу ($O(1)$ без алокації масиву): якщо колекція містить сотні тисяч елементів, уже надійно інкапсульована всередині сервісу й гарантовано ніколи не модифікується, створення обгортки за $O(1)$ без виділення десятків мегабайтів під копію масиву є цілком виправданим.
  2. Наявність елементів null: якщо за контрактом бізнес-логіки в колекції допустимі елементи null, виклик List.copyOf() викине NullPointerException, тоді як Collections.unmodifiableList() коректно збереже і дозволить безпечно читати список із null-елементами.

🎯 Шпаргалка для інтерв’ю

Головне про UnmodifiableList

  • Патерн: Decorator (View).
  • Складність: $O(1)$ за часом створення та додатковою пам’яттю.
  • Обмеження: забороняє операції запису через себе, але не захищає від мутацій базового списку.
  • Багатопотоковість: НЕ є потокобезпечним. Паралельна зміна оригінального списку призведе до ConcurrentModificationException.
  • Маркер RandomAccess: зберігається для швидкого індексного пошуку.

Порівняння: UnmodifiableList vs List.copyOf

| Характеристика | Collections.unmodifiableList(orig) | List.copyOf(orig) | | :— | :— | :— | | Принцип роботи | Обгортка (View) поверх оригіналу | Повноцінна захисна копія | | Складність створення | $O(1)$ | $O(N)$ (або $O(1)$ якщо вже immutable) | | Ізоляція від змін | ❌ Ні (пов’язаний з оригіналом) | ✅ Повна ізоляція | | Ставлення до null | Дозволяє null | Забороняє null (викидає NPE) |

Червоні прапорці на співбесіді (Чого говорити не можна)

  • ❌ «UnmodifiableList копіює елементи списку в новий масив» — груба помилка: це просто декоратор, що зберігає посилання.
  • ❌ «UnmodifiableList потокобезпечний, і його можна сміливо віддавати в інші потоки» — ні, при зміні оригіналу паралельний ітератор впаде з CME.
  • ❌ «UnmodifiableList захищає оригінальний список від додавання елементів» — ні, оригінальний список можна вільно модифікувати безпосередньо за оригінальним посиланням.

Пов’язані питання