Вопрос 7 · Раздел 8

Что делает операция flatMap()?

Используется когда у вас есть вложенные структуры (коллекция коллекций):

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

flatMap() — это промежуточная операция, которая делает два шага:

  1. Map: превращает каждый элемент в коллекцию/стрим
  2. Flat: соединяет все эти стримы в один длинный поток

Аналогия: как если бы вы взяли несколько стопок бумаг и сложили их в одну.

Используется когда у вас есть вложенные структуры (коллекция коллекций):

List<List<String>> nested = List.of(
    List.of("a", "b"),
    List.of("c", "d")
);

// flatMap "сплющивает" в один стрим
List<String> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());
// Результат: ["a", "b", "c", "d"]

// Извлечение всех тегов из постов
List<String> allTags = posts.stream()
    .flatMap(post -> post.getTags().stream())
    .distinct()
    .collect(Collectors.toList());

Простое правило: map = 1-к-1, flatMap = 1-ко-многим.

🟡 Middle Level

Визуализация процесса

  1. Исходный стрим: [A, B]
  2. Маппинг: A -> [a1, a2], B -> [b1, b2]
  3. Без flatMap (просто map): [[a1, a2], [b1, b2]] — стрим стримов
  4. С flatMap: [a1, a2, b1, b2] — единый стрим

Когда использовать flatMap

  • Объект содержит коллекцию (Order -> Stream<OrderItem>)
  • Работа с внешними источниками, возвращающими стримы
  • Нужно отфильтровать и трансформировать одновременно (возврат Stream.empty() для ненужных)

Optional.flatMap

В Optional flatMap используется для цепочек безопасных вызовов, возвращающих Optional:

Optional<String> email = user
    .flatMap(User::getContact)
    .flatMap(Contact::getEmail);

Избегает Optional<Optional<String>>.

Primitive flatMap

Всегда используйте flatMapToInt, flatMapToLong, flatMapToDouble чтобы избежать автобоксинга:

int[] allNumbers = lists.stream()
    .flatMapToInt(list -> list.stream().mapToInt(Integer::intValue))
    .toArray();

Когда НЕ использовать flatMap

  1. Простая фильтрация — используйте filter(), не нужно «разворачивать» элементы
  2. Один-к-одному — используйте map(), flatMap избыточен
  3. Просто нужно действие для каждогоforEach() чище

🔴 Senior Level

Управление ресурсами в flatMap

Критический момент: если функция открывает ресурс (Files.lines(path)), этот ресурс должен быть закрыт.

Stream API гарантирует вызов close() у всех стримов, созданных внутри flatMap, если закрывается основной стрим (через try-with-resources):

try (Stream<Path> paths = Files.list(dir)) {
    paths.flatMap(path -> {
        try {
            return Files.lines(path);
        } catch (IOException e) {
            return Stream.empty();
        }
    }).forEach(System.out::println);
}

// flatMap гарантирует close вложенных стримов: // когда внешний стрим закрывается, закрываются ВСЕ вложенные. // Это работает через AutoCloseable механизм Stream API.

Lazy Evaluation и Short-circuiting

flatMap полностью ленив. Если после flatMap стоит limit(1), как только первый элемент из первого внутреннего стрима будет получен — остальные стримы даже не создадутся.

Overhead

Каждый вызов flatMap создает новый объект стрима. В экстремально нагруженных циклах это может создать давление на память по сравнению с простым for.

Edge Cases

  • Empty Streams: Если маппер возвращает Stream.empty() — элемент исчезает (аналог filter)
  • Null Streams: Если маппер вернет null вместо стрима — получите NullPointerException. Всегда возвращайте Stream.empty()

Parallelism

flatMap в параллельных стримах работает менее эффективно, чем map — разделение задач становится менее предсказуемым из-за переменного количества элементов на выходе.

Диагностика

Отладка flatMap сложна. Используйте peek() внутри лямбды flatMap, чтобы видеть, какой элемент вызвал проблему.


🎯 Шпаргалка для интервью

Обязательно знать:

  • flatMap = Map (каждый элемент → стрим) + Flat (объединение всех стримов в один)
  • map = 1-к-1, flatMap = 1-ко-многим
  • Возвращает Stream<R>, маппер принимает T и возвращает Stream<R>
  • Optional.flatMap избегаёт вложенности Optional<Optional<T>>
  • Для примитивов: flatMapToInt, flatMapToLong, flatMapToDouble — избегают автобоксинга
  • Маппер никогда не должен возвращать null — только Stream.empty()

Частые уточняющие вопросы:

  • Когда flatMap фильтрует элементы? — Когда маппер возвращает Stream.empty() для ненужных
  • Как flatMap управляет ресурсами (Files.lines)? — Автоматически закрывает вложенные стримы при закрытии внешнего
  • flatMap + limit(1) — сколько стримов создастся? — Только первый внутренний, остальные не создадутся (ленивость)
  • Почему flatMap менее эффективен в параллелизме? — Переменное число элементов на выходе усложняет splitting

Красные флаги (НЕ говорить):

  • «flatMap и map одинаковы» — map возвращает один объект, flatMap — стрим
  • «Маппер flatMap может вернуть null» — нет, будет NullPointerException
  • «flatMap гарантирует порядок элементов» — порядок зависит от источника, не от flatMap
  • «flatMap всегда дороже map по памяти» — не всегда, зависит от количества элементов на выходе

Связанные темы:

  • [[8. В чём разница между map() и flatMap()]]
  • [[4. Что делает операция map()]]
  • [[3. Что делает операция filter()]]
  • [[2. В чём разница между intermediate и terminal операциями]]