Питання 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 операціями]]