Вопрос 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.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 для защиты от concurrent модификаций
// Для этого — 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 коллекцию]]