Stream.flatMap: примеры (JAVA)
Stream.flatMap(Function super T, ? extends Stream extends R>> mapper): StreamОписание Stream.flatMap
Метод flatMap интерфейса java.util.stream.Stream преобразует элементы входного потока в вложенные потоки и затем объединяет ("сплющивает") эти вложенные потоки в один результирующий поток. При обработке иерархических или вложенных коллекций данная операция позволяет получить единый поток элементов нижнего уровня.
Сигнатура основного варианта:
StreamflatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Пояснение аргументов и результата:
- mapper - функция, принимающая элемент типа T и возвращающая Stream из элементов типа R (или подпроизвольного подтипа R). mapper не должен возвращать null; допустим пустой поток для отсутствия элементов.
- Возвращаемое значение - Stream<R> с последовательностью элементов, полученных из всех внутренних потоков в порядке, соответствующем источнику (если источник упорядочен и не нарушена упорядоченность).
Вариации и родственные сигнатуры в JDK:
- Для примитивных потоков доступны соответствующие методы в IntStream, LongStream, DoubleStream: например, IntStream.flatMap(IntFunction<? extends IntStream> mapper). Они принимают функцию, возвращающую примитивный поток и возвращают примитивный поток того же вида.
- Альтернативные приёмы включают map (если требуется простое отображение без сплющивания) и методы mapToInt/mapToLong/mapToDouble для перехода к примитивным потокам.
Поведение и ограничения:
- mapper не должен возвращать null; если mapper вернёт null - будет выброшено NullPointerException в момент выполнения.
- flatMap поддерживает ленивость: промежуточные операции не выполняются до терминальной операции.
- При параллельной обработке flatMap сохраняет семантику, но внутренние mapper-функции должны быть без состояния или потокобезопасны, иначе возникают ошибки или некорректные результаты.
- flatMap сам по себе не выполняет сортировку; оригинальная упорядоченность сохраняется, если исходный стрим упорядочен и не используются операции, нарушающие порядок.
Примеры использования flatMap
Несколько коротких примеров, показывающих типичные варианты применения и ожидаемые результаты.
1) Сплющивание списка списков
List<List<String>> lists = List.of(List.of("a","b"), List.of("c"));
List<String> flat = lists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println(flat);
[a, b, c]
2) Разбиение строк на слова и объединение всех слов
List<String> lines = List.of("hello world","java stream");
List<String> words = lines.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList());
System.out.println(words);
[hello, world, java, stream]
3) Преобразование Optional в поток (Java 9+)
Optional<String> maybe = Optional.of("x");
List<String> out = Stream.of(maybe)
.flatMap(Optional::stream)
.collect(Collectors.toList());
System.out.println(out);
[x]
4) Примитивный поток - IntStream.flatMap
IntStream.of(1,2)
.flatMap(i -> IntStream.of(i, i*10))
.forEach(System.out::print);
1 10 2 20
5) Пример с пустым внутренним потоком
List<List<Integer>> data = List.of(List.of(), List.of(5));
List<Integer> res = data.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(res);
[5]
Похожая функциональность в Java
Набор операций, близких по смыслу:
- map - отображает каждый элемент в другой элемент, но не объединяет вложенные потоки. Используется при простом преобразовании элементов.
- mapToInt/mapToLong/mapToDouble - сохраняют более эффективную работу с примитивами при переходе к примитивному потоку.
- IntStream.flatMap/LongStream.flatMap/DoubleStream.flatMap - аналоги для примитивных потоков, когда внутренние элементы также примитивы.
- mapMulti (введён в более поздних версиях JVM) - альтернатива, дающая возможность более эффективного добавления нуля или нескольких элементов из одного входного элемента без создания промежуточного потока, полезна для производительности в горячих местах.
- Collectors.flatMapping - при агрегации внутри Collectors, позволяет выполнить плоское отображение в процессе группировки (полезно для комбинированных collector-цепочек).
Когда что предпочтительнее:
- Если внутренняя функция естественно возвращает Stream - плоское преобразование через flatMap естественно и выразительно.
- Если требуется высокопроизводительное добавление нескольких элементов без создания Stream для каждого исходного элемента, mapMulti может быть предпочтительнее.
- При работе с примитивами лучше использовать примитивные flatMap у IntStream/LongStream/DoubleStream для избежания автоупаковки.
Аналоги в других языках и отличия
Короткие сравнения с примерами и результатами.
JavaScript (ES2019) - Array.prototype.flatMap
const arr = [[1,2],[3]];
console.log(arr.flatMap(x => x));
[1, 2, 3]
Python - itertools.chain.from_iterable и генераторы
import itertools
lists = [[1,2],[3]]
print(list(itertools.chain.from_iterable(lists)))
[1, 2, 3]
Python отличается тем, что часто используются генераторы или выражения списка: [y for x in lists for y in x].
C# - LINQ SelectMany
var lists = new List<List<int>> { new(){1,2}, new(){3}};
var flat = lists.SelectMany(x => x);
Console.WriteLine(string.Join(" ", flat));
1 2 3
PHP - array_merge + array_map или array_reduce
$lists = [[1,2],[3]];
$result = array_merge(...$lists);
print_r($result);
Array
(
[0] => 1
[1] => 2
[2] => 3
)
Go - явные циклы
lists := [][]int{{1,2},{3}}
var res []int
for _, l := range lists {
res = append(res, l...)
}
fmt.Println(res)
[1 2 3]
Kotlin - flatMap
val lists = listOf(listOf(1,2), listOf(3))
println(lists.flatMap { it })
[1, 2, 3]
SQL - UNNEST или CROSS APPLY
-- пример PostgreSQL
SELECT elem
FROM (VALUES (ARRAY[1,2]), (ARRAY[3])) AS t(arr)
CROSS JOIN UNNEST(arr) AS elem;
elem ---- 1 2 3
Отличия от Java: в функциональных языках и средах плоское отображение часто встроено как оператор массива/коллекции, в Java требуется использование Stream API или примитивных stream-интерфейсов; также важна ленивость в Java streams и влияние параллельности.
Типичные ошибки при использовании flatMap
Частые причины неправильной работы и примеры.
1) Возвращение null из mapper
Stream.of("a").flatMap(s -> null).count();
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:228)
at java.base/java.util.stream.ReferencePipeline.flatMap(ReferencePipeline.java:XXX)
...
mapper должен возвращать Stream, а не null. Для отсутствия элементов следует возвращать Stream.empty().
2) Неправильная смешанная работа с примитивами
Stream.of(1,2)
.flatMap(i -> IntStream.of(i).boxed()) // лишняя упаковка
.mapToInt(Integer::intValue)
.sum();
Работает, но содержит лишние преобразования и упаковку/распаковку, что снижает производительность.
3) Параллельность и состояние
List<List<Integer>> data = ...;
List<Integer> out = data.parallelStream()
.flatMap(list -> list.stream().peek(i -> someState.increment()))
.collect(Collectors.toList());
Возможные гонки и некорректные значения someState при отсутствии синхронизации.
4) Ожидание немедленного выполнения
Stream<String> s = Stream.of("a","b").flatMap(x -> Arrays.stream(x.split("")));
// никаких действий до терминальной операции
Никакого вывода или вычислений до вызова терминальной операции (collect, forEach и т.д.).
Изменения и появления смежных возможностей
Ключевые дополнения в последних релизах, влияющие на работу со сплющиванием потоков:
- Optional.stream() и Stream.ofNullable() (Java 9) - позволяют проще превращать Optional или потенциально null-значения в поток и затем применять flatMap.
- mapMulti (введён в более поздних версиях Java) - предоставляет альтернативу flatMap, позволяя добавлять ноль или несколько элементов без создания промежуточного Stream и часто давая лучшую производительность в горячих участках.
- Поддержка дополнительных методов в Collectors, облегчающих комбинирование flat-проекций в агрегирующие операции (коллекторы постоянно расширяются в новых версиях JDK).
Сам метод flatMap оставался стабильным с момента введения Stream API, изменения касались преимущественно утилит и дополнительных облегчений в API потоков.
Расширенные и необычные примеры использования
1) Группировка с вложенной коллекцией и извлечением плоского списка через Collectors.flatMapping
Map<String, List<String>> map = List.of(
new AbstractMap.SimpleEntry<>("g1", List.of("a","b")),
new AbstractMap.SimpleEntry<>("g1", List.of("c")),
new AbstractMap.SimpleEntry<>("g2", List.of("x"))
).stream().collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.flatMapping(e -> e.getValue().stream(), Collectors.toList())
));
System.out.println(map);
{g1=[a, b, c], g2=[x]}
2) Использование flatMap для генерации комбинаций (декартово произведение)
List<String> a = List.of("x","y");
List<String> b = List.of("1","2");
List<String> pairs = a.stream()
.flatMap(s1 -> b.stream().map(s2 -> s1 + s2))
.collect(Collectors.toList());
System.out.println(pairs);
[x1, x2, y1, y2]
3) Сплющивание последовательности Optional и фильтрация отсутствующих значений
List<Optional<String>> list = List.of(Optional.of("a"), Optional.empty(), Optional.of("b"));
List<String> res = list.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
System.out.println(res);
[a, b]
4) Производительный вариант с mapMulti (для Java 16+)
List<List<Integer>> data = ...;
List<Integer> res = data.stream()
.mapMulti((lst, consumer) -> lst.forEach(consumer))
.collect(Collectors.toList());
// по сути эквивалентно flatMap(Collection::stream) но без создания промежуточных Stream для каждого элемента
Результат: единый список всех чисел из вложенных списков.
5) Параллельное сплющивание с сохранением порядка
List<List<Integer>> lists = IntStream.range(0,100)
.mapToObj(i -> IntStream.range(0,10).boxed().collect(Collectors.toList()))
.collect(Collectors.toList());
List<Integer> out = lists.parallelStream()
.flatMap(Collection::stream) // при collect(Collectors.toList()) порядок сохраняется, но затраты на синхронизацию
.collect(Collectors.toList());
System.out.println(out.size());
1000
В параллельной обработке рекомендуется оценивать влияние на порядок и синхронизацию; для больших объёмов может потребоваться другая стратегия разбивки данных.
6) Генерация независимых потоков ввода-вывода и последующее объединение
List<Path> files = List.of(Paths.get("file1.txt"), Paths.get("file2.txt"));
List<String> allLines = files.stream()
.flatMap(path -> {
try {
return Files.lines(path);
} catch (IOException e) {
return Stream.empty();
}
})
.collect(Collectors.toList());
System.out.println(allLines.size());
Количество строк суммарно из файлов (зависит от содержимого)
Важно закрывать ресурсы, здесь Files.lines возвращает Stream, требующий закрытия; использование try-with-resources или явное управление ресурсами предпочтительнее в production-коде.