Нужно получить плоский список токенов из строк. Какую операцию следует применить вместо указанной, чтобы результат имел тип List<String> и содержал a, b, c?
import java.util.*;
import java.util.stream.*;
List<String> result = Stream.of("a,b", "c")
.map(s -> Arrays.stream(s.split(",")))
.toList();
Нужно заменить map на flatMap. map преобразует каждый исходный элемент в один результат, поэтому здесь создаётся Stream<Stream<String>>; flatMap объединяет элементы всех вложенных потоков в один Stream<String>.
Исправленный вариант вернёт список [a, b, c].
Обычная операция map хорошо описывает преобразование «один элемент на один элемент»: например, строки в их длины или объекты в идентификаторы. На практике часто встречается преобразование «один элемент в несколько»: строка может содержать набор токенов, заказ — несколько позиций, а каталог — несколько дочерних элементов.
Если использовать только map, вложенные структуры сохраняются. Для композиции таких преобразований в Stream API предусмотрена операция flatMap, совмещающая отображение и последующее выравнивание результата.
В исходном коде функция, переданная в map, возвращает Stream<String>. Поэтому каждый элемент исходного потока превращается не в строку, а в отдельный поток строк.
Фактический тип результата до терминальной операции — Stream<Stream<String>>. Это не плоский список токенов: вложенные потоки становятся элементами внешнего потока, что приводит к неправильному типу результата и не позволяет напрямую работать с отдельными строками.
map применяет функцию ровно один раз к каждому элементу и сохраняет кардинальность на уровне элементов внешнего потока:
flatMap ожидает функцию, возвращающую поток, а затем последовательно включает элементы каждого такого потока в общий поток:
Вызов split создаёт массив токенов для каждой исходной строки, Arrays.stream превращает его во вложенный поток, а flatMap удаляет один уровень вложенности. Терминальная операция toList() собирает уже плоский Stream<String>.
Это не означает, что flatMap рекурсивно выравнивает произвольную глубину вложенности: он убирает только один уровень, созданный функцией отображения. Для Stream<Stream<Stream<T>>> потребуется ещё одна операция выравнивания либо другая модель преобразования.
Операция остаётся ленивой: функция для элемента вызывается по мере продвижения терминальной операции. Порядок элементов для обычного упорядоченного последовательного потока сохраняется: сначала токены первой строки, затем второй. В параллельной обработке порядок зависит от операции и характеристик потока; если порядок важен, его нельзя бездумно заменять на неупорядоченную обработку.
Сервис получает строки с несколькими тегами и должен построить множество всех тегов. Вариант с map создаёт поток потоков, поэтому дальнейшая фильтрация тегов потребовала бы сначала вручную обходить каждый вложенный поток. Это усложняет код и повышает риск ошибочного повторного использования потоков.
Вариант с flatMap сразу формирует плоский поток, после чего можно применить filter, distinct и сбор в множество. Альтернативой был бы обычный вложенный цикл: он проще для пошаговой отладки, но хуже выражает конвейер преобразований и смешивает обход с обработкой данных.
Выбран flatMap, потому что задача естественно является преобразованием «одна строка — много тегов». Результат получается компактным и композиционным:
При больших или ленивых источниках следует учитывать стоимость создания вложенных потоков и обработки каждого элемента. Если преобразование выполняет блокирующие операции или имеет побочные эффекты, сам выбор flatMap не делает его безопасным и не обеспечивает параллелизм.
Что произойдёт, если функция flatMap вернёт null?
В соответствии с контрактом flatMap такой результат трактуется как пустой поток. Это не превращает null в элемент итогового потока: соответствующий исходный элемент просто не добавит значений. Тем не менее обычно лучше явно возвращать Stream.empty(), чтобы намерение было очевидным и код не зависел от небрежной обработки null.
Закрываются ли вложенные потоки после обработки?
Да, поток, созданный функцией flatMap, закрывается после обработки его элементов. Если вложенный поток связан с ресурсом, его обработчик закрытия будет вызван при завершении обработки этого вложенного потока. Однако внешний поток и внешние ресурсы всё равно должны закрываться своим владельцем; flatMap не заменяет корректное управление ресурсами через try-with-resources.
Почему flatMap не всегда лучше вложенного map с последующим сбором?
flatMap предпочтителен, когда нужна потоковая обработка элементов вложенных результатов: он позволяет сразу фильтровать, ограничивать и собирать отдельные значения. Если же вложенные потоки нужно сохранить как самостоятельные объекты, например для независимого управления ими, выравнивание разрушит требуемую структуру. Кроме того, сложное преобразование с ресурсами или выраженной бизнес-логикой иногда понятнее реализовать обычным циклом.