Что такое Collections.unmodifiableList()
Метод Collections.unmodifiableList(List extends T> list) возвращает обёртку над переданным списком, которая запрещает любые прямые изменения его структуры.
🟢 Junior Level
1. Назначение и базовое поведение
Метод Collections.unmodifiableList(List<? extends T> list) возвращает обёртку над переданным списком, которая запрещает любые прямые изменения его структуры.
При попытке вызвать любой метод модификации (add, remove, set, clear) выбрасывается исключение UnsupportedOperationException:
List<String> original = new ArrayList<>();
original.add("Java");
original.add("Kotlin");
List<String> unmodifiable = Collections.unmodifiableList(original);
// Операции чтения разрешены:
System.out.println(unmodifiable.get(0)); // "Java"
// Операции изменения запрещены:
unmodifiable.add("Scala"); // Выбросит java.lang.UnsupportedOperationException!
unmodifiable.remove(0); // Выбросит java.lang.UnsupportedOperationException!
2. Принципиальный факт: это View (представление), а не копия
[!WARNING]
Collections.unmodifiableList()не копирует элементы в новый массив. Он создает легковесную проекцию (View) поверх исходного списка за время $O(1)$. Если исходный список будет изменен напрямую, эти изменения мгновенно станут видны в «неизменяемой» обертке!
original.add("Scala"); // Изменяем оригинал в обход обертки
// Изменение сразу же видно через unmodifiable-обертку:
System.out.println(unmodifiable.size()); // 3!
System.out.println(unmodifiable.get(2)); // "Scala"
🟡 Middle Level
1. Поверхностная неизменяемость (Shallow Immutability)
unmodifiableList защищает исключительно структуру самого списка (нельзя добавить, удалить или заменить ссылки на элементы), но не защищает состояние самих объектов, находящихся внутри списка:
class User {
private String name;
public User(String name) { this.name = name; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
List<User> list = new ArrayList<>();
list.add(new User("Алексей"));
List<User> unmodUsers = Collections.unmodifiableList(list);
// Мутация внутреннего состояния объекта РАЗРЕШЕНА:
unmodUsers.get(0).setName("Взломщик"); // Сработает без ошибок!
System.out.println(unmodUsers.get(0).getName()); // "Взломщик"
Чтобы добиться истинной глубокой неизменяемости (Deep Immutability), элементы списка должны быть спроектированы как неизменяемые сущности (Immutable Objects / Records).
2. Сравнение: unmodifiableList против List.copyOf и List.of
С выходом Java 9 и Java 10 появились современные способы создания по-настоящему неизменяемых списков:
| Характеристика | Collections.unmodifiableList(list) |
List.copyOf(collection) (Java 10+) |
List.of(e1, e2...) (Java 9+) |
|---|---|---|---|
| Тип структуры | Неизменяемое представление (View) | Настоящая неизменяемая копия (Copy) | Настоящий неизменяемый список |
| Связь с оригиналом | ✅ Есть (изменения оригинала видны) | ❌ Нет (полная изоляция) | Оригинала нет |
| Сложность создания | $O(1)$ (нет копирования данных) | $O(N)$ (копирует массив) | $O(N)$ / $O(1)$ |
Поддержка null |
Зависит от базового списка | ❌ Запрещен (NullPointerException) |
❌ Запрещен (NPE) |
| Основное применение | Возврат View без аллокаций |
Защитное копирование (Defensive Copy) | Фиксированные константы |
🔴 Senior Level
1. Сохранение маркерного интерфейса RandomAccess
Эффективность работы многих алгоритмов стандартной библиотеки (например, Collections.binarySearch()) зависит от наличия интерфейса-маркера RandomAccess:
- Для
RandomAccessсписков (например,ArrayList) алгоритм использует доступ по индексу за $O(1)$. - Для списков без
RandomAccess(например,LinkedList) алгоритм переключается на итераторы, чтобы не деградировать до $O(N^2)$.
Разработчики JDK учли это в реализации Collections.unmodifiableList():
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
Если исходный список реализует RandomAccess, обёртка гарантированно сохраняет эту оптимизацию.
2. Утечки памяти и проблемы конкурентности
- Неожиданный
ConcurrentModificationExceptionв многопоточности: Если Поток 1 производит чтениеfor (String s : unmodifiableList), а Поток 2 в это время вызываетoriginalList.add("X"), Поток 1 получитConcurrentModificationException. Обертка не обеспечивает никакой потокобезопасности, так как счетчик модификацийmodCountисходного списка изменился! - Скрытая утечка памяти (Memory Retention):
Если долгоживущий сервис держит ссылку на
unmodifiableList, он удерживает ссылку на весь исходный объект списка и все его элементы в памяти Heap, даже если они больше не нужны внешнему коду. - Оптимизация в
List.copyOf(): Если передать вList.copyOf(collection)коллекцию, которая уже является неизменяемым списком, созданным черезList.of()илиList.copyOf(), метод не делает копирование, а возвращает ту же самую ссылку за $O(1)$:List<String> l1 = List.of("A", "B"); List<String> l2 = List.copyOf(l1); System.out.println(l1 == l2); // true!
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«
Collections.unmodifiableList()возвращает декоратор списка, блокирующий любые попытки структурной модификации (add,remove,set) выбросомUnsupportedOperationException. Принципиально важно помнить, что это View (представление), а не изолированная копия: создание занимает $O(1)$ времени и памяти, но любые изменения в исходном списке мгновенно видны через обертку. Кроме того, обертка обеспечивает только поверхностную неизменяемость — поля объектов внутри списка по-прежнему можно менять. В современной Java для создания полностью изолированных копий используетсяList.copyOf()».
3 каверзных вопроса на интервью с ответами
1. В чём разница между понятиями «Unmodifiable Collection» и «Immutable Collection» в Java?
Ответ:
- Unmodifiable (Неизменяемая для модификации): коллекцию нельзя изменить через ссылку на обертку (
unmodifiableList), но ее состояние может измениться из-за модификации базовой коллекции, на которую оформлена проекция. - Immutable (Абсолютно неизменяемая): состояние коллекции и ссылки на элементы гарантированно не могут измениться ни при каких обстоятельствах во всей JVM (как у
List.of()илиList.copyOf()).
2. Что произойдет, если вызвать unmodifiableList.iterator().remove()?
Ответ: Вызов завершится исключением UnsupportedOperationException. Метод iterator() обертки возвращает специальный декоратор UnmodifiableIterator, у которого методы remove(), add() и set() переопределены и безусловно выбрасывают исключение.
3. Почему в защищенном коде (Security-Critical API) опасно возвращать Collections.unmodifiableList(internalList)?
Ответ:
- Если злоумышленник имеет доступ к классу сервиса через рефлексию, он может прочитать приватное поле
listвнутри обертки и получить прямой доступ к исходному мутабельному списку. - Если сам сервис продолжает изменять
internalList, клиент может столкнуться с гонками данных иConcurrentModificationException. Для безопасной инкапсуляции необходимо возвращать изолированную копию черезList.copyOf()илиnew ArrayList<>(internalList).
Типичные ошибки и Red Flags
- ❌ Утверждение, что
unmodifiableListкопирует данные в памяти: Это $O(1)$ View на исходную коллекцию. - ❌ Вера в то, что элементы списка становятся неизменяемыми: Защищается только структура списка, а не поля объектов внутри.
- ❌ Считать
unmodifiableListпотокобезопасным: Изменение исходного списка параллельным потоком вызоветConcurrentModificationExceptionу читателя обертки. - ❌ Передача
nullвList.copyOf(): В отличие отunmodifiableList,List.copyOfне терпитnullни в самой ссылке, ни среди элементов.