📦 Раздел 4 · Вопрос #24

Как работает Collections.unmodifiableList() внутри

Класс Collections.unmodifiableList() устроен как классический Декоратор (Decorator / Wrapper). Он не создает новый список и не копирует элементы в памяти. Вместо этого он возвра...


🟢 Junior Level

1. Архитектурный принцип

Класс Collections.unmodifiableList() устроен как классический Декоратор (Decorator / Wrapper). Он не создает новый список и не копирует элементы в памяти. Вместо этого он возвращает объект специального внутреннего класса Collections.UnmodifiableList, который держит прямую ссылку на исходный список.

  • Методы чтения (get, size, contains, indexOf) прозрачно перенаправляются базовому списку.
  • Методы модификации (add, set, remove, clear) переопределены и безусловно выбрасывают UnsupportedOperationException.
List<String> rawList = new ArrayList<>();
rawList.add("A");

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

unmodifiable.get(0); // Делегирует вызов: rawList.get(0) -> возвращает "A"
unmodifiable.add("B"); // Блокирует вызов: throw new UnsupportedOperationException()

2. Схема делегирования вызовов

                 Вызов get(0)
  Клиент ────────────────────────► UnmodifiableList (Обертка)
                                            │
                                            │ Делегирует: list.get(0)
                                            ▼
                                     ArrayList (Оригинал)
                                            │
                                  Возвращает элемент

                 Вызов add("X")
  Клиент ────────────────────────► UnmodifiableList (Обертка)
                                            │
                                            ▼
                                  throw new UnsupportedOperationException()

🟡 Middle Level

1. Разбор исходного кода OpenJDK

Внутри java.util.Collections определена цепочка вложенных статических классов:

// Базовый декоратор для всех коллекций:
static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
    @SuppressWarnings("serial")
    final Collection<? extends E> c; // Ссылка на оригинал

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

    // Читающие операции делегируют:
    public int size()                   { return c.size(); }
    public boolean isEmpty()            { return c.isEmpty(); }
    public boolean contains(Object o)   { return c.contains(o); }

    // Мутирующие операции блокируются:
    public boolean add(E e) {
        throw new UnsupportedOperationException();
    }
    public boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }
    public void clear() {
        throw new UnsupportedOperationException();
    }

    // Итератор оборачивается в unmodifiable-проекцию:
    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();
            }
        };
    }
}

Класс UnmodifiableList наследует UnmodifiableCollection и переопределяет списочные методы:

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 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();
    }

    // Защита методов Java 8:
    public void replaceAll(UnaryOperator<E> operator) {
        throw new UnsupportedOperationException();
    }
    public void sort(Comparator<? super E> c) {
        throw new UnsupportedOperationException();
    }
}

2. Динамический выбор реализации: оптимизация RandomAccess

В фабричном методе Collections.unmodifiableList() есть ключевое ветвление:

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ?
            new UnmodifiableRandomAccessList<>(list) :
            new UnmodifiableList<>(list));
}
  • Если передать ArrayList, метод вернет UnmodifiableRandomAccessList (реализует интерфейс-маркер RandomAccess). Алгоритмы вроде Collections.binarySearch() будут использовать быстрый доступ по индексу за $O(1)$.
  • Если передать LinkedList, метод вернет базовый UnmodifiableList (без RandomAccess). Алгоритмы не будут делать индексные вызовы, предотвращая деградацию до $O(N^2)$.

🔴 Senior Level

1. JIT-компиляция и накладные расходы делегирования

На уровне машинного кода накладные расходы вызова unmodifiableList.get(i) практически равны нулю:

  • При вызове метода обертки происходит дополнительный вызов list.get(index).
  • В горячих циклах JIT-компилятор JVM (C2 Compiler) выполняет инлайнинг (Method Inlining): тело метода get() обертки встраивается прямо в место вызова.
  • Благодаря мономорфному профилированию вызовов (Monomorphic Call Site) косвенный вызов превращается в прямое чтение из памяти за 1 такт процессора. Время работы идентично прямому обращению к ArrayList.

2. Уязвимость к рефлексии и защита через JPMS (Java 9+)

Поскольку ссылка на оригинальный список хранится в final-поле list, в Java 8 и ниже существовала известная брешь безопасности:

List<String> secret = new ArrayList<>(List.of("password123"));
List<String> publicView = Collections.unmodifiableList(secret);

// Обход защиты через Reflection (Java 8):
Field field = publicView.getClass().getDeclaredField("list");
field.setAccessible(true);
List<String> backdoor = (List<String>) field.get(publicView);
backdoor.add("hacked"); // Успешно изменили оригинальный список!

Решение в современной Java:

  1. Java Platform Module System (Java 9+): Пакет java.util в модуле java.base закрыт для глубокой рефлексии. Попытка вызова field.setAccessible(true) выбросит InaccessibleObjectException.
  2. Истинные неизменяемые коллекции (List.of, List.copyOf): Классы ImmutableCollections.ListN хранят элементы в собственных компактных массивах и не имеют ссылки на мутабельный список.

3. Механика subList() и listIterator()

  • Метод subList(int fromIndex, int toIndex) возвращает новую обертку UnmodifiableList или UnmodifiableRandomAccessList вокруг среза базового списка list.subList(from, to). Попытка изменить подсписок также немедленно заблокируется.
  • Метод listIterator() возвращает UnmodifiableListIterator, в котором методы add(e), set(e) и remove() переопределены с выбросом UnsupportedOperationException. Навигационные методы (next, previous, nextIndex, previousIndex) работают штатно.

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«Внутри Collections.unmodifiableList() работает по паттерну «Декоратор». Он создает легковесный экземпляр UnmodifiableList (или UnmodifiableRandomAccessList при поддержке произвольного доступа), который сохраняет ссылку на исходный список. Все читающие методы прозрачно делегируют вызовы оригиналу, а любые мутирующие методы (add, set, remove, sort, replaceAll) сразу выбрасывают UnsupportedOperationException. Накладные расходы на вызовы стремятся к нулю благодаря JIT-инлайнингу, однако обертка не изолирует данные: изменения в исходном списке по-прежнему видны через представление».


3 каверзных вопроса на интервью с ответами

1. Почему в Java 8 методы replaceAll() и sort() пришлось явно переопределять в UnmodifiableList?

Ответ: В Java 8 в интерфейсе List появились default-методы sort() и replaceAll(). По умолчанию они мутируют список через ListIterator.set(). Если бы разработчики JDK не переопределили их в UnmodifiableList, вызов unmodifiableList.sort() начал бы переставлять элементы, и исключение UnsupportedOperationException вылетело бы только в середине сортировки при первой попытке вызова set(), оставив коллекцию в поврежденном полу-отсортированном состоянии. Явное переопределение гарантирует немедленный отказ (Fail-Fast) до начала любых операций.

2. Зачем нужен отдельный класс UnmodifiableRandomAccessList?

Ответ: Интерфейс RandomAccess — маркерный. Если бы существовал только один класс UnmodifiableList, реализующий RandomAccess, то при оборачивании LinkedList алгоритмы посчитали бы его списком с быстрым индексным доступом и начали бы вызывать get(i), что привело бы к квадратичной сложности $O(N^2)$. Если бы UnmodifiableList не реализовывал RandomAccess, то ArrayList потерял бы статус быстрого списка. Поэтому создано два взаимодополняющих класса.

3. Наследует ли UnmodifiableList сериализуемость базового списка?

Ответ: Класс UnmodifiableCollection напрямую реализует интерфейс java.io.Serializable. Однако если переданный в него базовый список не был сериализуемым (например, самописная реализация List без Serializable), то при попытке сериализовать обертку через ObjectOutputStream будет выброшено исключение java.io.NotSerializableException.


Типичные ошибки и Red Flags

  • ❌ Утверждение, что unmodifiableList() копирует данные в массив: Это обёртка, хранящая только ссылку на оригинал.
  • ❌ Игнорирование того, что subList() тоже защищен: Обертка рекурсивно возвращает unmodifiable-подсписки.
  • ❌ Предположение о защите от многопоточных модификаций: Изменение исходного списка другим потоком вызовет ConcurrentModificationException в итераторе обертки.
  • ❌ Мнение, что listIterator() позволяет модифицировать список: Итератор списка также обернут и блокирует set и add.

Связанные темы