1. Вступ
Уявімо, що маємо список студентів, а в кожного — список його захоплень. Наприклад:
List<List<String>> hobbies = List.of(
List.of("Плавання", "Шахи"),
List.of("Футбол"),
List.of("Програмування", "Читання", "Кіно")
);
Ваше завдання — отримати єдиний список усіх захоплень, аби зрозуміти, чим загалом цікавляться студенти. Логічно спробувати скористатися map:
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
Що ми отримали? Список потоків! Тобто Stream<Stream<String>>. Це не зовсім те, що потрібно — ми хотіли б отримати просто Stream<String>, щоб працювати з кожним захопленням безпосередньо.
Уявіть, що у вас є коробка, у якій лежать інші коробки з іграшками. Метод map просто дістає кожну коробку й показує вам самі коробки (Stream<Stream<String>>). А ви хотіли б одразу бачити всі іграшки (Stream<String>), не порпаючись у коробках.
2. flatMap: як «розгорнути» вкладені колекції
Щоб «розпакувати коробки» та отримати плоский потік, потрібен метод flatMap.
Він приймає функцію, що для кожного елемента повертає потік, і одразу «сплющує» всі вкладені потоки в один.
Синтаксис flatMap
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Простіше кажучи: flatMap очікує, що ви для кожного елемента повернете потік (Stream), а він об’єднає їх у один великий плоский потік.
Приклад: об’єднуємо всі захоплення студентів
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Плавання, Шахи, Футбол, Програмування, Читання, Кіно]
Для кожного списку захоплень ми викликали list.stream() (отримали потік захоплень одного студента). А flatMap об’єднав усі потоки в один — тепер ми бачимо всі захоплення, а не потоки захоплень.
Візуальна схема
hobbies.stream()
|
|---> [Плавання, Шахи] -> stream
|---> [Футбол] -> stream
|---> [Програмування, ...] -> stream
|
flatMap: об’єднує усе в один Stream<String>
Чому map не розв’язує завдання?
Якби ми використали map, то отримали б Stream<Stream<String>>, і працювати з цим незручно: наприклад, не можна одразу перебрати всі рядки — доведеться виконувати додаткові обходи.
3. Практичні приклади використання flatMap
Розбиття рядків на символи (List<String> → Stream<Character>)
Припустімо, що у вас є список рядків, і ви хочете отримати потік усіх символів, які трапляються в цих рядках:
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
Пояснення:
- word.chars() повертає IntStream (потік кодів символів).
- mapToObj перетворює коди на символи.
- flatMap об’єднує всі отримані потоки символів у один.
Робота з Optional: Stream<Optional<T>> → Stream<T>
Припустімо, що у вас є список об’єктів Optional, і ви хочете отримати потік лише тих значень, які справді присутні:
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
Особливість:
Optional<T> (починаючи з Java 9) має метод stream(), який повертає або порожній потік, або потік з одного елемента. flatMap об’єднує всі непорожні значення в один потік.
4. Новий метод mapMulti: коли flatMap не потрібен
Чому з’явився mapMulti?
Метод mapMulti, що з’явився у Java 16, працює подібно до flatMap, але дещо ефективніше й гнучкіше.
flatMap — потужний інструмент, та має недолік: для кожного елемента ви зобов’язані створювати новий Stream, навіть якщо повертаєте 0, 1 або кілька елементів. Це може бути неефективно, особливо коли ви просто хочете «розгорнути» елементи без створення проміжних колекцій чи потоків.
mapMulti — це вдосконалена альтернатива flatMap, яка дозволяє безпосередньо додавати потрібні значення до результативного потоку через Consumer, не створюючи проміжних структур. Замість того, щоб повертати потік, ви прямо вказуєте, які елементи додати до результативного потоку через Consumer.
Синтаксис mapMulti
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
Для кожного елемента викликається функція mapper, яка отримує сам елемент і Consumer, до якого можна «складати» нові значення.
Приклад: фільтрація та розгортання елементів за один прохід
Припустімо, у вас є список чисел, і ви хочете для парних чисел додати їх до потоку двічі, а для непарних — жодного разу (тобто фільтрація й «розгортання» одночасно):
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // Додаємо двічі
}
// Для непарних нічого не робимо (фільтрація)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
Порівняння з flatMap
Те саме завдання через flatMap довелося б написати так:
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
Тут ми все одно змушені створювати Stream.of(...) або Stream.empty() для кожного елемента, навіть якщо це неефективно.
5. Коли використовувати flatMap, а коли mapMulti?
- map — коли ви перетворюєте кожен елемент на один елемент.
- flatMap — коли ви перетворюєте кожен елемент на потік елементів.
- mapMulti — коли ви хочете згенерувати кілька елементів без створення проміжного потоку (ефективніше, особливо в «гарячих» циклах).
Ще приклад: розгортання Map<Integer, List<String>> у Stream<Pair(Integer, String)>
Припустімо, маємо мапу: id → список захоплень.
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("Плавання", "Шахи"),
2, List.of("Футбол"),
3, List.of("Програмування", "Читання", "Кіно")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Плавання, Шахи, Футбол, Програмування, Читання, Кіно]
А якщо ми хочемо отримати пари (id, хобі):
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: Плавання, 1: Шахи, 2: Футбол, 3: Програмування, 3: Читання, 3: Кіно]
Через mapMulti (Java 16+):
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
Перевага:
mapMulti не створює проміжних потоків, а безпосередньо додає значення до підсумкового потоку.
Візуалізація: flatMap і mapMulti
| Метод | Що повертає функція | Як об’єднує | Проміжні колекції? |
|---|---|---|---|
|
Один елемент | Не об’єднує | Ні |
|
Потік (Stream) | Об’єднує | Так (проміжні потоки) |
|
Consumer (0..n разів) | Об’єднує | Ні (додає безпосередньо) |
6. Типові помилки при роботі з flatMap і mapMulti
Помилка № 1: Отримали Stream<Stream<T>> замість Stream<T>. Часто студенти використовують map замість flatMap, коли працюють із колекціями колекцій. У результаті доводиться писати зайві цикли.
Помилка № 2: Неправильний тип значення, що повертається. Для flatMap функція має повертати Stream, а не List або масив.
Помилка № 3: Неефективність. У простих випадках flatMap працює чудово, але якщо для кожного елемента доводиться створювати Stream.of або Stream.empty(), це може бути надмірним. Для таких завдань краще використовувати mapMulti.
Помилка № 4: mapMulti не працює у старих версіях Java. mapMulti з’явився тільки в Java 16. Якщо у вас старіша версія JDK, цей метод буде недоступний.
Помилка № 5: Проблеми з null. Не повертайте null із flatMap — завжди повертайте Stream.empty() для «порожніх» випадків.
Помилка № 6: Проміжні колекції. Не створюйте зайві списки або потоки, якщо можна додати елементи безпосередньо через Consumer у mapMulti.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ