В практической ситуации результат Stream.toList() передают коду, который добавляет в него элементы: какое свойство результата делает такую операцию ошибочной?
Stream.toList() возвращает немодифицируемый список: операции добавления, удаления и замены его элементов завершаются исключением UnsupportedOperationException. Если нужен изменяемый результат, следует явно создать новый список, например через Collectors.toCollection(ArrayList::new).
Важно не переносить это свойство на Collectors.toList(): его контракт не гарантирует ни изменяемость, ни конкретную реализацию списка.
Изначально сбор результата обычно выполняли через Collectors.toList(). Этот коллектор предоставлял удобный способ получить список, но его спецификация намеренно оставляла реализацию и свойства изменяемости за пределами гарантированного контракта.
Метод Stream.toList() появился позднее как более короткий способ получить результат обычного потокового преобразования. Его контракт явно фиксирует немодифицируемость результата, что позволяет API эффективнее выбирать внутреннюю реализацию и снижает риск случайного изменения уже сформированного результата.
Ошибка возникает, когда разработчик воспринимает любой список после сбора как обычный ArrayList. Например, поток может корректно завершиться, но последующая модификация результата завершится UnsupportedOperationException уже во время выполнения.
Неверный выбор особенно опасен в слое, где результат передаётся другому компоненту: вызывающий код может не знать, каким способом список был создан. Если бизнес-логика требует добавления или удаления элементов, немодифицируемый результат нельзя использовать как рабочий буфер.
Stream.toList() собирает элементы в список, который нельзя структурно или функционально модифицировать через интерфейс List. Запрет относится к операциям вроде add, remove и set; чтение элементов и обычная итерация разрешены.
Минимальный пример:
Collectors.toList() не следует использовать как способ гарантированно получить изменяемый список: такая гарантия отсутствует в контракте коллектора, даже если конкретная реализация JDK обычно возвращает изменяемую структуру. Когда важна изменяемость, нужно явно указать поставщик коллекции через toCollection.
Когда результат должен быть защищён от изменений, Stream.toList() удобен и выразителен. Когда результат является промежуточным накопителем, лучше выбрать изменяемую коллекцию явно; это делает намерение кода очевидным и не зависит от не гарантированных свойств реализации.
Сервис формирует список идентификаторов через Stream.toList() и передаёт его в компонент, который добавляет служебный идентификатор. В рабочей среде возникает UnsupportedOperationException, хотя сбор самого потока проходит без ошибок.
Первый вариант — заменить вызов на Collectors.toList(). Он часто устраняет проблему в текущей реализации JDK, но не выражает формальную гарантию изменяемости и потому слабее с точки зрения контракта.
Второй вариант — обернуть результат в new ArrayList<>(...). Это явно создаёт изменяемую копию, но требует дополнительного прохода или копирования элементов.
Выбранное решение — сразу собирать результат через Collectors.toCollection(ArrayList::new), если компонент действительно должен изменять список. В результате контракт становится явным, поведение не зависит от конкретной реализации Collectors.toList(), а структура данных соответствует назначению.
Collectors.toList() изменяемый список?Нет. Контракт гарантирует сбор элементов в List, но не обещает изменяемость, конкретный тип, потокобезопасность или сериализуемость результата. Если эти свойства важны, их нужно задать явно другим коллектором или создать нужную коллекцию самостоятельно.
Stream.toList()?Немодифицируемость списка не означает глубокую неизменяемость объектов. Сам список нельзя перестроить через add, remove или set, но если его элементы являются изменяемыми объектами, состояние этих объектов может изменяться. Например, можно изменить поле объекта, находящегося в списке, если сам объект это допускает.
Collectors.toList()?Нужно явно выбрать поставщик коллекции: использовать Collectors.toCollection(ArrayList::new) или создать копию через конструктор ArrayList. Первый подход сразу задаёт способ накопления, второй полезен, когда исходный результат уже получен и требуется отделить изменяемую копию от исходной коллекции.