Stream.flatMap: примеры (JAVA)

Использование flatMap в Java Streams
Раздел: Потоки данных (Stream API) - промежуточные операции
Stream.flatMap(Function> mapper): Stream

Описание Stream.flatMap

Метод flatMap интерфейса java.util.stream.Stream преобразует элементы входного потока в вложенные потоки и затем объединяет ("сплющивает") эти вложенные потоки в один результирующий поток. При обработке иерархических или вложенных коллекций данная операция позволяет получить единый поток элементов нижнего уровня.

Сигнатура основного варианта:

Stream flatMap(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

Пример java
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 для генерации комбинаций (декартово произведение)

Пример java
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 и фильтрация отсутствующих значений

Пример java
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+)

Пример java
List<List<Integer>> data = ...;
List<Integer> res = data.stream()
    .mapMulti((lst, consumer) -> lst.forEach(consumer))
    .collect(Collectors.toList());
// по сути эквивалентно flatMap(Collection::stream) но без создания промежуточных Stream для каждого элемента
Результат: единый список всех чисел из вложенных списков.

5) Параллельное сплющивание с сохранением порядка

Пример java
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) Генерация независимых потоков ввода-вывода и последующее объединение

Пример java
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-коде.

джава Stream.flatMap function comments

En
Stream.flatMap Returns a stream consisting of the results of replacing each element with the contents of a mapped stream