Питання 24 · Розділ 4

Як працює Collections.unmodifiableList() всередині

Collections.unmodifiableList() повертає обгортку (wrapper) поверх вихідного списку, яка забороняє всі модифікуючі операції.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Collections.unmodifiableList() повертає обгортку (wrapper) поверх вихідного списку, яка забороняє всі модифікуючі операції.

List<String> mutable = new ArrayList<>();
mutable.add("A");
mutable.add("B");

List<String> unmodifiable = Collections.unmodifiableList(mutable);

unmodifiable.add("C"); // ❌ UnsupportedOperationException!
unmodifiable.get(0);   // ✅ OK — читання працює

Ключовий момент: unmodifiableList — це не копія, а обгортка. Якщо змінити оригінальний список, зміни будуть видні через обгортку:

mutable.add("C");
System.out.println(unmodifiable); // [A, B, C] — зміну видно!

🟡 Middle Level

Внутрішня реалізація

Collections.unmodifiableList() повертає об’єкт внутрішнього класу Collections.UnmodifiableList:

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

Два варіанти обгортки:

  • UnmodifiableRandomAccessList — якщо список підтримує довільний доступ (ArrayList)
  • UnmodifiableList — якщо ні (LinkedList)

Це оптимізація продуктивності: RandomAccess-списки використовують індексний доступ O(1), а не ітератор.

Структура UnmodifiableList

static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                                  implements List<E> {
    private final List<? extends E> list;

    UnmodifiableList(List<? extends E> list) {
        super(list);
        this.list = list;  // посилання на оригінал, не копія!
    }

    // Усі модифікуючі методи кидають UnsupportedOperationException
    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 E get(int index) {
        return list.get(index);  // прямий виклик до оригіналу
    }
    public int size() {
        return list.size();
    }
}

UnmodifiableList vs List.copyOf (Java 10+)

Характеристика unmodifiableList() List.copyOf()
Копіювання Ні (обгортка) Так (defensive copy)
Зміна оригіналу Видно через обгортку Не впливає
Null елементи Дозволені Заборонені
Тип повернення UnmodifiableList ImmutableCollections.ListN
Версія Java З Java 1.2 З Java 10

RandomAccess — маркерний інтерфейс, що означає O(1) доступ за індексом. Defensive copy — копіювання даних для захисту від зовнішніх змін.

List<String> original = new ArrayList<>(List.of("A", "B"));

// unmodifiableList — обгортка
List<String> wrapper = Collections.unmodifiableList(original);
original.add("C");
System.out.println(wrapper);  // [A, B, C] — зміну видно!

// List.copyOf — копія
List<String> copy = List.copyOf(original);
original.add("D");
System.out.println(copy);     // [A, B, C] — зміну НЕ видно

ListIterator обгортка

ListIterator<String> it = unmodifiable.listIterator();
it.next();           // OK
it.set("X");         // ❌ UnsupportedOperationException
it.add("Y");         // ❌ UnsupportedOperationException
it.remove();         // ❌ UnsupportedOperationException

Повертається UnmodifiableListIterator, який також блокує всі модифікації.

Продуктивність

Операція ArrayList напряму unmodifiableList
get(i) O(1) O(1) (один виклик делегування)
size() O(1) O(1) (делегування)
contains() O(n) O(n) (делегування)
iterator() O(1) O(1) (обгортка ітератора)

Overhead мінімальний: один рівень делегування (~1-2 ns на виклик).


🔴 Senior Level

Глибокий аналіз вихідного коду JDK

// Collections.java (OpenJDK)

static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
    final Collection<? extends E> c;

    UnmodifiableCollection(Collection<? extends E> c) {
        if (c == null) throw new NullPointerException();
        this.c = c;
    }

    // Усі mutating-методи кидають виняток:
    public boolean add(E e)     { throw new UnsupportedOperationException(); }
    public boolean remove(Object o) { throw new UnsupportedOperationException(); }
    public void clear()         { throw new UnsupportedOperationException(); }

    // read-методи делегують:
    public int size()           { return c.size(); }
    public boolean isEmpty()    { return c.isEmpty(); }
    public boolean contains(Object o) { return c.contains(o); }
    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(); }
        };
    }
}

UnmodifiableRandomAccessList оптимізація

static class UnmodifiableRandomAccessList<E>
    extends UnmodifiableList<E>
    implements RandomAccess {

    UnmodifiableRandomAccessList(List<? extends E> list) {
        super(list);
    }

    // Перевизначає subList — теж повертає unmodifiable
    public List<E> subList(int from, int to) {
        return new UnmodifiableRandomAccessList<>(
            list.subList(from, to)
        );
    }
}

Ця оптимізація важлива: UnmodifiableList (не-RandomAccess) конвертує індексні операції в ітераторні, що для ArrayList було б O(n) замість O(1).

Через рефлексію можна змінити!

List<String> list = new ArrayList<>(List.of("A", "B"));
List<String> unmod = Collections.unmodifiableList(list);

// Через рефлексію — доступ до поля `list` в UnmodifiableList
Field field = unmod.getClass().getDeclaredField("list");
field.setAccessible(true);
List<String> underlying = (List<String>) field.get(unmod);
underlying.add("C");  // Зміна обходить захист!

System.out.println(unmod); // [A, B, C]

Це не баг, а особливість wrapper-підходу. List.copyOf() (Java 10+) вирішує цю проблему, створюючи справжню незмінювану колекцію.

Якщо ваш проект на Java 8 — List.copyOf() недоступний. Використовуйте Collections.unmodifiableList(new ArrayList<>(original)) як defensive copy.

Серіалізація

UnmodifiableList реалізує Serializable. При десеріалізації:

// Відновлюється обгортка з посиланням на десеріалізований список
// Якщо оригінал змінився після серіалізації обгортки — це НЕ вплине

Коли використовувати

Сценарій Рекомендація
API для читання, оригінал контролюється вами unmodifiableList()
Повна іммутабельність, Java 10+ List.copyOf()
Thread-safe незмінюваний список List.copyOf() або unmodifiableList() + синхронізація оригіналу
Глибока іммутабельність (вкладені об’єкти) Жоден із цих методів — використовуйте immutable DTO

Best Practices

// ✅ Повернення unmodifiable з публічного API
public List<String> getNames() {
    return Collections.unmodifiableList(names);
}

// ✅ Java 10+ — List.copyOf для повної іммутабельності
public List<String> getNames() {
    return List.copyOf(names);
}

// ❌ Не використовуйте unmodifiableList для захисту від конкурентних модифікацій
// Для цього — CopyOnWriteArrayList або Collections.synchronizedList()

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

Обов’язково знати:

  • unmodifiableList() повертає UnmodifiableRandomAccessList або UnmodifiableList залежно від RandomAccess
  • Усі модифікуючі методи кидають UnsupportedOperationException
  • Читаючі методи делегують оригіналу напряму — overhead 1-2 ns
  • subList() теж повертає unmodifiable обгортку
  • Через рефлексію можна дістатися до оригіналу і обійти захист
  • List.copyOf() (Java 10+) — справжня іммутабельність, defensive copy, без зв’язку з оригіналом
  • Iterator/ListIterator також обгорнуті в unmodifiable-версії (remove/set/add заборонені)

Часті уточнюючі запитання:

  • Чому два класи — UnmodifiableList і UnmodifiableRandomAccessList? — Оптимізація: RandomAccess-списки отримують індексний доступ O(1), решта — через ітератор.
  • Чи можна обійти захист unmodifiableList? — Так, через рефлексію (getDeclaredField("list")). List.copyOf() вирішує цю проблему.
  • Який overhead при виклику методів обгортки? — Один рівень делегування, ~1-2 ns — практично нульовий.
  • Що поверне subList() unmodifiable-списку? — Ще одну unmodifiable-обгортку на подсписок.

Червоні прапорці (НЕ говорити):

  • «unmodifiableList створює копію колекції» — ні, це wrapper-патерн, зберігає посилання на оригінал
  • «Захист від змін гарантований на 100%» — рефлексія дозволяє обійти
  • «unmodifiableList thread-safe» — ні, синхронізацію потрібно забезпечувати окремо
  • «Iterator від unmodifiableList дозволяє видаляти елементи» — ні, UnmodifiableListIterator теж блокує модифікації

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

  • [[23. Що таке Collections.unmodifiableList()]]
  • [[25. В чому різниця між Iterator та ListIterator]]
  • [[22. Як отримати synchronized колекцію]]