Как работает Collections.unmodifiableList() внутри
Collections.unmodifiableList() возвращает обёртку (wrapper) поверх исходного списка, которая запрещает все модифицирующие операции.
🟢 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 коллекцию]]