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?
Появившийся в Java 16 метод mapMulti работает похоже на 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) | Склеивает | Да (промежуточные 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ