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

В чём разница между map() и flatMap()?

Главное отличие — что возвращает функция-маппер:

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

🟢 Junior Level

Главное отличие — что возвращает функция-маппер:

Критерий map() flatMap()
Когда Преобразование 1→1 Преобразование 1→N + объединение
Возврат маппера R (один объект) Stream<R> (стрим объектов)
Соответствие 1-к-1 1-ко-многим
Вложенность Нет Схлопывает вложенные стримы

map(Function<T, R>):

  • Функция возвращает один объект типа R
  • Соответствие 1-к-1
    List<String> words = List.of("hello", "world");
    words.stream().map(String::length)  // [5, 5]
    

flatMap(Function<T, Stream<R>>):

  • Функция возвращает стрим объектов типа R
  • Соответствие 1-ко-многим (0, 1 или много)
    List<List<String>> nested = List.of(List.of("a", "b"), List.of("c"));
    nested.stream().flatMap(Collection::stream)  // ["a", "b", "c"]
    

Простое правило: Если у вас коллекция внутри элемента — flatMap, если простое преобразование — map.

🟡 Middle Level

Сигнатуры

// map — принимает T, возвращает R
<R> Stream<R> map(Function<T, R> mapper)

// flatMap — принимает T, возвращает Stream<R>
<R> Stream<R> flatMap(Function<T, Stream<R>> mapper)

Когда выбирать map

  • Трансформация данных (User -> UserDTO)
  • Извлечение поля (User -> User.getEmail())
  • Тип меняется, но структура линейная

Когда выбирать flatMap

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

Optional: особенная разница

Optional<User> user = ...;

// map вернёт Optional<Optional<String>> — вложенность!
Optional<Optional<String>> bad = user.map(u -> u.getEmailAsync());

// flatMap вернёт Optional<String> — правильно
Optional<String> good = user.flatMap(u -> u.getEmailAsync());

В Optional flatMap “схлопывает” вложенность.

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

  • НЕ flatMap, если вам нужно просто преобразовать элемент — используйте map()
  • НЕ map, если элемент превращается в коллекцию и вы хотите один плоский результат — map() даст Stream<List<T>>, а вам нужен Stream<T>
  • НЕ оба, если вам просто нужно отфильтровать — filter() читаемее

🔴 Senior Level

Производительность

Memory Overhead: flatMap дороже — для каждого элемента создается новый объект Stream. При миллионах элементов это создает нагрузку на Young Gen.

Short-Circuiting: flatMap поддерживает короткое замыкание. Если после него стоит .findFirst(), выполнение прервется после первого элемента из первого вложенного стрима.

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

Null Handling

  • В map: возврат null создает стрим с null-элементом
  • В flatMap: возврат null вместо стрима → NullPointerException

Infinite Streams

flatMap может объединять бесконечные стримы, но терминальная операция не завершится (если нет короткого замыкания).

Диагностика

  • Type Checking: Если видите Stream<List<String>> — скорее всего забыли flatMap
  • Testing: Обязательно тестируйте случаи когда маппер возвращает пустой стрим и стрим с одним null-элементом

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

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

  • map(Function<T, R>) — возврат одного объекта R, соответствие 1-к-1
  • flatMap(Function<T, Stream<R>>) — возврат стрима объектов, схлопывание вложенности, 1-ко-многим
  • map для трансформации данных (User → UserDTO), flatMap для извлечения коллекций (Order → Stream)
  • В Optional: map может создать Optional<Optional<T>>, flatMap всегда возвращает плоский Optional<T>
  • flatMap дороже по памяти — создаёт новый Stream для каждого элемента
  • Оба поддерживают short-circuiting: findFirst() прервёт выполнение после первого элемента

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

  • Когда map даст Stream<List>? — Когда маппер извлекает коллекцию из элемента, а нужен Stream → используйте flatMap
  • null в map vs flatMap? — map пропустит null дальше, flatMap бросит NullPointerException
  • Когда flatMap медленнее? — При миллионах элементов из-за аллокации Stream-объектов
  • Можно ли flatMap заменить filter? — Да, возвратом Stream.empty() для отфильтрованных, но filter читаемее

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

  • «map и flatMap взаимозаменяемы» — нет, разные сигнатуры и семантика
  • «flatMap быстрее map» — нет, flatMap всегда дороже из-за создания Stream-объектов
  • «map может вернуть 0 элементов» — нет, map всегда возвращает 1 объект (или null)
  • «flatMap гарантирует порядок в параллельных стримах» — нет, splitting менее предсказуем

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

  • [[4. Что делает операция map()]]
  • [[7. Что делает операция flatMap()]]
  • [[9. Что такое параллельные стримы]]
  • [[2. В чём разница между intermediate и terminal операциями]]