Питання 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<T>>? — Коли маппер вилучає колекцію з елемента, а потрібен 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 операціями]]