Как работает 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:
- Java Platform Module System (Java 9+): Пакет
java.utilв модулеjava.baseзакрыт для глубокой рефлексии. Попытка вызоваfield.setAccessible(true)выброситInaccessibleObjectException. - Истинные неизменяемые коллекции (
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.