Що робить операція flatMap()?
Використовується коли у вас є вкладені структури (колекція колекцій):
🟢 Junior Level
flatMap() — це проміжна операція, яка робить два кроки:
- Map: перетворює кожен елемент на колекцію/стрім
- 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
Візуалізація процесу
- Вихідний стрім:
[A, B] - Мапінг:
A -> [a1, a2],B -> [b1, b2] - Без flatMap (просто map):
[[a1, a2], [b1, b2]]— стрім стрімів - З 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
- Проста фільтрація — використовуйте
filter(), не потрібно «розгортати» елементи - Один-до-одного — використовуйте
map(), flatMap надмірний - Просто потрібна дія для кожного —
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 операціями]]