JavaRush /Курсы /JAVA 25 SELF /Методы flatMap и mapMulti

Методы flatMap и mapMulti

JAVA 25 SELF
32 уровень , 0 лекция
Открыта

1. Введение

Давайте представим, что у нас есть список студентов, и у каждого студента есть список его увлечений. Например:

List<List<String>> hobbies = List.of(
    List.of("Плавание", "Шахматы"),
    List.of("Футбол"),
    List.of("Программирование", "Чтение", "Кино")
);

Ваша задача: получить единый список всех увлечений, чтобы узнать, чем вообще интересуются студенты. Логично попробовать использовать map:

List<Stream<String>> streams = hobbies.stream()
    .map(list -> list.stream())
    .collect(Collectors.toList());

Что мы получили? Список потоков! То есть Stream<Stream<String>>. Это не совсем то, что нужно — мы хотели бы получить просто Stream<String>, чтобы работать с каждым увлечением напрямую.

Представьте, что у вас есть коробка, в которой лежат другие коробки с игрушками. Метод map просто достаёт каждую коробку и показывает вам коробку (Stream<Stream<String>>). А вы хотели бы сразу видеть все игрушки (Stream<String>), не ковыряясь в коробках.

2. flatMap: как «развернуть» вложенные коллекции

Чтобы «распаковать коробки» и получить плоский поток, нужен метод flatMap.
Он принимает функцию, которая для каждого элемента возвращает поток, и сразу «сплющивает» все вложенные потоки в один.

Синтаксис flatMap

Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

Проще говоря: flatMap ждёт, что вы для каждого элемента вернёте поток (Stream), а он всё это «разгладит» в один большой поток.

Пример: объединяем все увлечения студентов

List<String> allHobbies = hobbies.stream()
    .flatMap(list -> list.stream())
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [Плавание, Шахматы, Футбол, Программирование, Чтение, Кино]

Для каждого списка увлечений мы вызвали list.stream() (получили поток увлечений одного студента). Ну а flatMap «разгладил» все потоки в один большой поток — теперь мы видим все увлечения, а не потоки увлечений.

Визуальная схема

hobbies.stream()
    |
    |---> [Плавание, Шахматы]    -> stream
    |---> [Футбол]               -> stream
    |---> [Программирование, ...]-> stream
    |
flatMap: объединяет всё в один Stream<String>

Почему map не решает задачу?

Если бы мы использовали map, то получили бы Stream<Stream<String>>, и работать с этим неудобно: например, нельзя сразу перебрать все строки, нужно делать дополнительные обходы.

3. Практические примеры использования flatMap

Разбивка строк на символы (List<String> → Stream<Character>)

Допустим, у вас есть список строк, и вы хотите получить поток всех символов, встречающихся во всех строках:

List<String> words = List.of("Java", "Stream");

List<Character> characters = words.stream()
    .flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
    .collect(Collectors.toList());

System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]

Объяснение:

  • word.chars() возвращает IntStream (поток кодов символов).
  • mapToObj превращает коды в символы.
  • flatMap объединяет все полученные потоки символов в один.

Работа с Optional: Stream<Optional<T>> → Stream<T>

Допустим, у вас есть список Optional-объектов, и вы хотите получить поток только тех значений, которые действительно присутствуют:

List<Optional<String>> optionals = List.of(
    Optional.of("Java"),
    Optional.empty(),
    Optional.of("Stream")
);

List<String> present = optionals.stream()
    .flatMap(opt -> opt.stream())
    .collect(Collectors.toList());

System.out.println(present);
// [Java, Stream]

Фишка:
Optional<T> (начиная с Java 9) имеет метод stream(), который возвращает либо пустой поток, либо поток из одного элемента. flatMap объединяет все непустые значения в один поток.

4. Новый метод mapMulti: когда flatMap не нужен

Почему появился mapMulti?

Появившийся в Java 16 метод mapMulti работает похоже на flatMap, но чуть эффективнее и гибче.

flatMap — мощный инструмент, но у него есть недостаток: для каждого элемента вы обязаны создать новый Stream, даже если хотите вернуть 0, 1 или несколько элементов. Это может быть неэффективно, особенно если вы просто хотите «развернуть» элементы без создания промежуточных коллекций или потоков.

mapMulti — это улучшенная версия flatMap, которая позволяет напрямую добавлять нужные значения в результирующий поток через Consumer, не создавая промежуточных структур. Вместо того чтобы возвращать поток, вы напрямую указываете, какие элементы добавить в результирующий поток через Consumer.

Синтаксис mapMulti

<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)

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

Пример: фильтрация и разворачивание элементов за один проход

Допустим, у вас есть список чисел, и вы хотите для чётных чисел добавить их в поток дважды, а для нечётных — ни разу (то есть, фильтрация и «разворачивание» одновременно):

List<Integer> numbers = List.of(1, 2, 3, 4);

List<Integer> result = numbers.stream()
    .mapMulti((number, consumer) -> {
        if (number % 2 == 0) {
            consumer.accept(number);
            consumer.accept(number); // Добавляем дважды
        }
        // Для нечётных ничего не делаем (фильтрация)
    })
    .collect(Collectors.toList());

System.out.println(result);
// [2, 2, 4, 4]

Сравнение с flatMap

Ту же задачу через flatMap пришлось бы писать так:

List<Integer> result = numbers.stream()
    .flatMap(number -> number % 2 == 0
        ? Stream.of(number, number)
        : Stream.empty())
    .collect(Collectors.toList());

Здесь мы всё равно вынуждены создавать Stream.of(...) или Stream.empty() для каждого элемента, даже если это неэффективно.

5. Когда использовать flatMap, когда mapMulti?

  • map — когда вы преобразуете каждый элемент в один элемент.
  • flatMap — когда вы преобразуете каждый элемент в поток элементов.
  • mapMulti — когда вы хотите сгенерировать несколько элементов без создания промежуточного потока (эффективнее, особенно в горячих циклах).

Ещё пример: разворачивание Map<Integer, List<String>> в Stream<Pair(Integer, String)>

Допустим, у нас есть карта: id → список увлечений.

Map<Integer, List<String>> studentHobbies = Map.of(
    1, List.of("Плавание", "Шахматы"),
    2, List.of("Футбол"),
    3, List.of("Программирование", "Чтение", "Кино")
);

List<String> allHobbies = studentHobbies.values().stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

System.out.println(allHobbies);
// [Плавание, Шахматы, Футбол, Программирование, Чтение, Кино]

А если мы хотим получить пары (id, хобби):

List<String> pairs = studentHobbies.entrySet().stream()
    .flatMap(entry -> entry.getValue().stream()
        .map(hobby -> entry.getKey() + ": " + hobby))
    .collect(Collectors.toList());

System.out.println(pairs);
// [1: Плавание, 1: Шахматы, 2: Футбол, 3: Программирование, 3: Чтение, 3: Кино]

Через mapMulti (Java 16+):

List<String> pairs = studentHobbies.entrySet().stream()
    .mapMulti((entry, consumer) -> {
        for (String hobby : entry.getValue()) {
            consumer.accept(entry.getKey() + ": " + hobby);
        }
    })
    .collect(Collectors.toList());

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

Визуализация: flatMap и mapMulti

Метод Что возвращает функция Как объединяет Промежуточные коллекции?
map
Один элемент Просто Нет
flatMap
Поток (Stream) Склеивает Да (промежуточные Stream)
mapMulti
Consumer (0..n раз) Склеивает Нет (добавляет напрямую)

6. Типичные ошибки при работе с flatMap и mapMulti

Ошибка №1: Получили Stream<Stream<T>> вместо Stream<T>. Часто студенты используют map вместо flatMap, когда работают с коллекциями коллекций. В результате приходится писать лишние циклы.

Ошибка №2: Неправильный тип возвращаемого значения. Для flatMap функция должна возвращать Stream, а не List или массив.

Ошибка №3: Неэффективность. В простых случаях flatMap работает отлично, но если для каждого элемента приходится создавать Stream.of или Stream.empty(), это может быть избыточно. Для таких задач лучше использовать mapMulti.

Ошибка №4: mapMulti не работает в старых версиях Java. mapMulti появился только в Java 16. Если у вас более старая версия JDK, этот метод будет недоступен.

Ошибка №5: Проблемы с null. Не возвращайте null из flatMap — всегда возвращайте Stream.empty() для «пустых» случаев.

Ошибка №6: Промежуточные коллекции. Не создавайте лишние списки или потоки, если можно добавить элементы напрямую через consumer в mapMulti.

1
Задача
JAVA 25 SELF, 32 уровень, 0 лекция
Недоступна
Сбор всех увлечений студентов для школьных клубов 📚
Сбор всех увлечений студентов для школьных клубов 📚
1
Задача
JAVA 25 SELF, 32 уровень, 0 лекция
Недоступна
Создание каталога хобби для каждого студента 🧑‍🎓
Создание каталога хобби для каждого студента 🧑‍🎓
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Zlyden' Уровень 59
29 ноября 2025
Вот и настал тот момент, когда я перестал понимать ваши лекции... то ли примеры неудачные, то ли я слишком стар для всего этого... печалька, до этого шёл бодрячком
Артемий Уровень 66
17 октября 2025
Не понятно почему нужно указывать явно тип результата .<String>mapMulti(...) В лекции был пример .mapMulti(...)
Anton Pohodin Уровень 26
23 октября 2025
Можно просто указать в объявлении списка List<Object> вместо List<String> и тогда не нужно будет приводить явно тип.
Andrey Уровень 1
27 сентября 2025
32