🔒 Раздел 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 защищает оригинальный список от добавления элементов» — нет, оригинальный список можно свободно модифицировать напрямую.

Связанные вопросы