W3docs

Немодифицируемые коллекции в Java

Создание неизменяемых коллекций в Java с помощью List.of, Set.of, Map.of и обёрток Collections.unmodifiable*.

Модифицируемая коллекция позволяет любому, у кого есть ссылка, изменять её содержимое. Немодифицируемая коллекция — нет: вызов add, remove, put, clear или set бросает UnsupportedOperationException. В Java есть два взаимодополняющих способа создать такую коллекцию: фабрики .of(...), появившиеся в Java 9 (List.of, Set.of, Map.of, Map.ofEntries), и старые обёртки Collections.unmodifiable*. На точке вызова они похожи, но отличаются в двух важных аспектах, и выбор инструмента зависит от того, что именно вам нужно.

Эта глава завершает часть о фреймворке коллекций, предоставляя вам чистый, современный рецепт для «дайте мне константу» и «дайте мне снимок».

Зачем нужна неизменяемость

Три конкретных преимущества, оправдывающих этот паттерн:

  1. Безопасное совместное использование. Передайте немодифицируемый список конструктору, рабочему потоку или потребителю событий, и вам не нужно беспокоиться о том, что они изменят ваше состояние. Тип на уровне компилятора не говорит «только для чтения», но среда выполнения — да.
  2. Безопасное хеширование. Помещать изменяемый List в HashSet — это ошибка: если содержимое списка изменится, изменится его hashCode, и множество потеряет этот элемент. Немодифицируемые коллекции полностью обходят эту проблему.
  3. Лучший дизайн API. Возврат немодифицируемого представления из геттера говорит: «это моё — читай, но не меняй». Без этого каждый вызывающий сам должен решать, делать ли защитную копию.

Два подхода

List.of, Set.of, Map.of, Map.ofEntriesпо-настоящему неизменяемые коллекции

Добавлены в Java 9. Они создают новую коллекцию со своим внутренним хранилищем. Ни у кого больше нет ссылки на него:

List<String> roles  = List.of("admin", "editor", "viewer");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> ages = Map.of("alice", 30, "bob", 25);

Map<String, Integer> many = Map.ofEntries(
    Map.entry("alice", 30),
    Map.entry("bob",   25),
    Map.entry("carol", 28)
);

Используйте их для констант и литералов — небольших фиксированных коллекций, которые вы вписываете в код. JIT компилирует их в очень компактные, малонакладные представления (часто в виде одного встроенного массива). Затраты на выделение памяти равны нулю сверх того, что занимает сам литерал.

Три ограничения, которые нужно помнить:

  1. Нет элементов null, нет ключей null, нет значений null. List.of("a", null) бросает NullPointerException при создании. Если нужно представить «отсутствие», используйте Optional или пропустите ключ в map.
  2. Нет дубликатов для Set.of и Map.of. Set.of("a", "a") бросает IllegalArgumentException. Они предназначены для литеральных данных, которые вы контролируете.
  3. Map.of имеет перегрузки только до 10 записей. Для 11 и более используйте Map.ofEntries(Map.entry(...), Map.entry(...), ...).

Collections.unmodifiableList(coll) и др. — представления существующей коллекции

Оборачивают коллекцию в представление только для чтения (view). Оригинал по-прежнему изменяем, и изменения через оригинал видны через представление:

List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
List<String> view    = Collections.unmodifiableList(mutable);

view.add("d");                      // throws UnsupportedOperationException
mutable.add("d");                   // legal — and the view sees the change
System.out.println(view);            // [a, b, c, d]

Используйте их, когда хотите открыть доступ к внутренней коллекции без копирования и без предоставления вызывающим права на изменение. Классический паттерн — геттер:

public List<String> getNames() {
  return Collections.unmodifiableList(this.names);
}

Вызывающий не сможет изменить this.names через возвращённое представление. Вы — сможете. Если хотите запретить и себе, скопируйте:

return List.copyOf(this.names);

…это и есть третий подход.

List.copyOf, Set.copyOf, Map.copyOf — снимок, затем заморозка

Сокращение для «скопировать текущее содержимое в новую неизменяемую коллекцию»:

List<String> snapshot = List.copyOf(mutable);

После этого вызова snapshot полностью независим от mutable. Последующие изменения mutable невидимы через snapshot. Есть и умная оптимизация: если источник уже является немодифицируемой коллекцией, созданной List.of / List.copyOf, вызов возвращает сам источник — без выделения памяти.

copyOf отклоняет элементы null, так же как of. Если источник может содержать null, используйте вместо этого Collections.unmodifiableList(new ArrayList<>(source)).

Три паттерна в сводке

ПаттернНезависим от источника?Допускает null?Используйте когда
List.of("a", "b")н/д (нет источника)НетЛитеральные константы
List.copyOf(source)Да — собственное хранилищеНетСнимок в момент времени
Collections.unmodifiableList(source)Нет — представлениеДаОткрыть внутреннее состояние только для чтения

Когда на точке вызова читается «это буквально жёстко закодированные данные?» — берите of. Когда читается «хочу замороженный снимок того, что есть прямо сейчас» — берите copyOf. Когда читается «хочу, чтобы текущее содержимое было видимым, но не изменяемым через эту ссылку» — берите unmodifiableList.

Поверхностная, а не глубокая неизменяемость

Все три подхода являются поверхностными — они замораживают структуру коллекции, но не элементы внутри неё.

List<int[]> arrays = List.of(new int[]{1, 2}, new int[]{3, 4});
arrays.add(new int[]{5});                 // UnsupportedOperationException
arrays.get(0)[0] = 99;                    // OK — and now the list contains {99, 2}

Если нужна глубокая неизменяемость, выбирайте типы элементов, которые сами по себе неизменяемы. Records с примитивными полями или полями типа String — да. Records с изменяемыми полями — нет. Это та же оговорка, что применяется к final-ссылкам в целом: привязка фиксирована, целевой объект — нет.

Set.of и Map.of имеют неопределённый порядок итерации

Два намеренных дизайнерских решения, которые часто удивляют:

  1. Set.of и Map.of намеренно рандомизируют порядок итерации между запусками одной JVM. Если вы пишете код, зависящий от определённого порядка этих коллекций, тесты будут нестабильными. Используйте List.of (который сохраняет порядок литерала) или LinkedHashSet/LinkedHashMap, обёрнутые в Collections.unmodifiable*, когда порядок действительно важен.
  2. Set.of(a, b) и Set.of(b, a) могут итерироваться по-разному даже в рамках одного запуска, если значения хешируются по-разному. Не сравнивайте через toString.

Это сделано намеренно — Java не даёт вам случайно зависеть от порядка, чтобы реализация была свободна его менять.

Что немодифицируемость не даёт

  • Она не обеспечивает потокобезопасность при чтении изменяемых полей элементов. Если элементы изменяемы и другой поток их изменяет, синхронизация необходима в любом случае.
  • Она не делает базовую коллекцию потокобезопасной. Collections.unmodifiableList(arrayList) — это представление не потокобезопасного списка; если другой поток делает add в arrayList, чтение через представление может увидеть разорванное состояние. Для потокобезопасной неизменяемости подходит List.copyOf (или List.of) — они владеют приватным хранилищем.
  • Она не делает .equals независимым от порядка. List, возвращённый List.of, по-прежнему сравнивается по позиции с другими списками, а не по содержимому.

Рабочий пример: литералы, снимки, представления и ловушка поверхностности

Программа ниже демонстрирует все три подхода рядом друг с другом, показывает сюрприз «представление видит мутации», обещание «копия независима» и ловушку поверхностности, которая удивляет всех в первый раз.

java— editable, runs on the server

Что следует вынести из запуска:

  • List.of и List.copyOf оба создают настоящую неизменяемую коллекцию — они отклоняют любые мутации. Разница лишь в том, задали ли вы данные литерально или скопировали откуда-то ещё.
  • Представление Collections.unmodifiableList отклонило view.add, но приняло backing.add через оригинальную ссылку. Изменения через базовый список стали видны через представление. Это определяющая особенность представления и причина, по которой этот подход не является заменой copyOf в недоверенном коде.
  • Ловушка поверхностности реальна: элементы int[] неизменяемого List<int[]> сами по себе изменяемы, и редактирование одного перезаписывает «замороженный» список. Если нужна глубокая неизменяемость, элементы сами должны быть неизменяемыми.
  • Set.of отклонил дубликат, а Map.of отклонил значение null — оба при создании. Эти коллекции быстро и громко сигнализируют об ошибках; это особенность.
  • List.copyOf уже неизменяемого списка вернул тот же экземпляр без выделения памяти. Это оптимизация JDK, и именно поэтому «всегда копировать на выходе» дёшево, когда источник уже неизменяем.

Что дальше — и вперёд к части 12

На этом закрывается часть Collections Framework. Теперь вы знаете все реализации (ArrayList, LinkedList, HashMap, TreeMap, очереди, деки и остальное), все интерфейсы (Collection, List, Set, Map, Queue, Deque), курсоры итерации (Iterator, ListIterator), интерфейсы упорядочивания (Comparable, Comparator), статический инструментарий (Collections) и историю с неизменяемостью.

Следующая часть — Functional Programming — кардинально меняет направление. Вместо как хранить данные она рассматривает как выражать преобразования данных. Первая глава, Functional Programming in Java, вводит ментальную модель: функции как значения, неизменяемость, чистые функции и композиция. Далее часть охватывает лямбды, ссылки на методы, встроенные функциональные интерфейсы (Function, Predicate, Consumer, Supplier), Optional и потоки — которые используют изученные вами коллекции в качестве источника и стока.

Большинство паттернов в этой части — list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), stream().filter(...).toList() — уже функциональны по духу. Часть 12 делает этот дух явным и показывает, как применять его ко всему остальному.

Практика

Практика
У вас есть поле `private List<String> names = new ArrayList<>()`, и вы хотите геттер, который позволяет вызывающим *читать* текущее содержимое, но не изменять его, и который также отражает последующие добавления в `names`, сделанные владельцем класса. Какое возвращаемое выражение подходит?
У вас есть поле `private List<String> names = new ArrayList<>()`, и вы хотите геттер, который позволяет вызывающим *читать* текущее содержимое, но не изменять его, и который также отражает последующие добавления в `names`, сделанные владельцем класса. Какое возвращаемое выражение подходит?
Was this page helpful?