Карта / Stream API и лямбды / flatMap и reduce

flatMap и reduce

flatMap — «разворачивает» вложенные коллекции в плоский стрим. Если map превращает каждый элемент в один другой, то flatMap превращает каждый элемент в несколько (или ни одного) и соединяет всё в один стрим.

// У каждого автора — список книг. Хотим все книги одним списком.
List<Author> authors = List.of(
    new Author("Толстой",   List.of("Война и мир", "Анна Каренина")),
    new Author("Чехов",     List.of("Вишнёвый сад", "Три сестры"))
);

// map даёт Stream<List<String>> — список списков ❌
// flatMap разворачивает в Stream<String> — один плоский поток ✅
List<String> allBooks = authors.stream()
    .flatMap(author -> author.books().stream())
    .toList();
// ["Война и мир", "Анна Каренина", "Вишнёвый сад", "Три сестры"]

reduce — свёртка: превращает весь стрим в одно значение, применяя операцию попарно:

// Сумма всех чисел
int sum = List.of(1, 2, 3, 4, 5).stream()
    .reduce(0, Integer::sum);  // 0 + 1 + 2 + 3 + 4 + 5 = 15
//           ↑ начальное значение (identity)

// Самое длинное слово
Optional<String> longest = List.of("Java", "Stream", "API").stream()
    .reduce((a, b) -> a.length() >= b.length() ? a : b);
// Optional — потому что если стрим пуст, результата нет
Копнуть глубже

flatMap vs map — как запомнить. map(f) где f: T → R даёт Stream<R>. Если f: T → List<R>, то map даст Stream<List<R>> — вложенный стрим. flatMap(f) «разворачивает» каждый List<R> и склеивает в один Stream<R>. Любой раз когда видите Stream<List<...>> или Stream<Optional<...>> — скорее всего нужен flatMap.

reduce и его три формы:

// 1. С identity (начальным значением) — всегда возвращает T
T reduce(T identity, BinaryOperator<T> accumulator)

// 2. Без identity — возвращает Optional<T> (стрим может быть пустым)
Optional<T> reduce(BinaryOperator<T> accumulator)

// 3. С combiner — для параллельных стримов где тип аккумулятора отличается
<U> U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)

Когда reduce, а когда collect. reduce для агрегации в одно значение (сумма, произведение, конкатенация, максимум). collect для сборки в контейнер (список, карту, строку через joining). На практике для стандартных операций часто есть готовый Collector или специализированный метод (count(), sum(), max()), который эффективнее ручного reduce.

// Не обязательно reduce — есть специализированные методы:
stream.mapToInt(Integer::intValue).sum();
stream.mapToInt(Integer::intValue).average();
stream.max(Comparator.naturalOrder());

Частый вопрос на собесе: «Чем отличается map от flatMap?» — map 1→1, flatMap 1→N и разворачивает. Попросят пример с вложенными коллекциями — используйте пример с авторами и книгами.

🎤 Закрыл тему, если можешь объяснить:
• разницу map и flatMap на конкретном примере с вложенными списками;
• что делает reduce и зачем нужно начальное значение (identity) (если дошёл до 2-го слоя).