📦 Раздел 4 · Вопрос #23

Что такое Collections.unmodifiableList()

Метод Collections.unmodifiableList(List 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. Утечки памяти и проблемы конкурентности

  1. Неожиданный ConcurrentModificationException в многопоточности: Если Поток 1 производит чтение for (String s : unmodifiableList), а Поток 2 в это время вызывает originalList.add("X"), Поток 1 получит ConcurrentModificationException. Обертка не обеспечивает никакой потокобезопасности, так как счетчик модификаций modCount исходного списка изменился!
  2. Скрытая утечка памяти (Memory Retention): Если долгоживущий сервис держит ссылку на unmodifiableList, он удерживает ссылку на весь исходный объект списка и все его элементы в памяти Heap, даже если они больше не нужны внешнему коду.
  3. Оптимизация в 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)?

Ответ:

  1. Если злоумышленник имеет доступ к классу сервиса через рефлексию, он может прочитать приватное поле list внутри обертки и получить прямой доступ к исходному мутабельному списку.
  2. Если сам сервис продолжает изменять internalList, клиент может столкнуться с гонками данных и ConcurrentModificationException. Для безопасной инкапсуляции необходимо возвращать изолированную копию через List.copyOf() или new ArrayList<>(internalList).

Типичные ошибки и Red Flags

  • ❌ Утверждение, что unmodifiableList копирует данные в памяти: Это $O(1)$ View на исходную коллекцию.
  • ❌ Вера в то, что элементы списка становятся неизменяемыми: Защищается только структура списка, а не поля объектов внутри.
  • ❌ Считать unmodifiableList потокобезопасным: Изменение исходного списка параллельным потоком вызовет ConcurrentModificationException у читателя обертки.
  • ❌ Передача null в List.copyOf(): В отличие от unmodifiableList, List.copyOf не терпит null ни в самой ссылке, ни среди элементов.

Связанные темы