Что такое 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
Многопоточные риски: Иллюзия потокобезопасности
Среди разработчиков распространено опасное заблуждение: «Если список немодифицируемый, его можно безопасно отдавать на чтение в параллельные потоки». Это не так!
ConcurrentModificationExceptionпри параллельной итерации:- Если Поток 1 читает коллекцию через
unmodifiableList, а Поток 2 в это время вызываетoriginalList.add(), счётчик модификацийmodCountбазовогоArrayListувеличивается. - Итератор Потока 1 мгновенно падает с
ConcurrentModificationExceptionпрямо во время вызоваnext().
- Если Поток 1 читает коллекцию через
- Отсутствие барьеров памяти (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() предпочтительнее в двух случаях:
- Экстремальная экономия памяти и времени ($O(1)$ без аллокаций массива): если коллекция содержит сотни тысяч элементов, уже надёжно инкапсулирована внутри сервиса и никогда не модифицируется, создание обёртки за $O(1)$ без выделения десятков мегабайт под копию массива оправдано.
- Наличие элементов
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 защищает оригинальный список от добавления элементов» — нет, оригинальный список можно свободно модифицировать напрямую.