Menu

Стримы в Java: filter, map, collect и reduce простыми словами

Как обрабатывать коллекции с помощью Stream API в Java - filter, map, sorted, collect, count и reduce - строя читаемые конвейеры вместо ручных циклов.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Конвейеры вместо циклов

Стрим - это конвейер для обработки последовательности значений. Вы начинаете с источника (обычно это коллекция), сцепляете операции вроде «оставить только эти», «преобразовать каждый», «отсортировать их» и заканчиваете тем, что собираете результат. Вместо того чтобы писать цикл с временным списком и if внутри, вы описываете, что хотите получить, и позволяете стриму выполнить весь обход.

Теперь, когда у вас есть лямбды, стримы встают на свои места: каждая операция принимает лямбду (или ссылку на метод), описывающую работу с одним элементом.

Читаем сверху вниз: взять имена, оставить длинные, перевести в верхний регистр, собрать в список. Исходный список names никогда не изменяется - стрим создаёт новый результат и оставляет источник нетронутым.

Анатомия стрима

У любого конвейера три части:

  • Источник - list.stream(), Arrays.stream(array) или Stream.of(a, b, c).
  • Ноль или более промежуточных операций - filter, map, sorted, distinct, limit. Каждая возвращает новый стрим, поэтому их можно сцеплять.
  • Ровно одна терминальная операция - collect, count, forEach, reduce, findFirst. Она запускает работу и выдаёт результат (или побочный эффект).

Это разделение важно из-за одного правила, которое поначалу удивляет всех.

Промежуточные операции ленивые

Промежуточные операции сами по себе не делают ничего. Они лишь запоминают, что должно произойти. Конвейер запускается только тогда, когда терминальная операция запрашивает результат.

Запустите: лямбда map ни разу не выполняется. Добавьте терминальную операцию - и вся цепочка оживает:

Эта ленивость - не причуда, с которой надо бороться: она позволяет стриму сливать операции и пропускать ненужную работу. Но это значит, что конвейер без терминальной операции ничего не делает, и это распространённая ошибка из разряда «почему ничего не происходит?».

filter и map: оставить и преобразовать

Эти две операции несут основную нагрузку. filter принимает Predicate (лямбду, возвращающую boolean) и оставляет элементы, которые её прошли. map принимает Function и заменяет каждый элемент результатом её применения.

Обратите внимание, что map может менять тип элемента: здесь стрим из String становится стримом из Integer. String::length - это ссылка на метод, сокращённая запись лямбды w -> w.length().

Терминальные операции выдают результат

Когда вы придали стриму нужную форму, терминальная операция превращает его во что-то конкретное:

Распространённые терминальные операции: collect (собрать в список/множество/map), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max и reduce. После выполнения терминальной операции стрим израсходован - повторно использовать его нельзя. Вызовите list.stream() снова, чтобы получить свежий конвейер.

Сбор результатов

collect с Collectors - главный рабочий инструмент для построения результата. Самый частый - Collectors.toList():

Collectors также даёт toSet(), joining(...), groupingBy(...) и counting(). В Java 16 и новее можно заменить collect(Collectors.toList()) на более короткий .toList() (он возвращает неизменяемый список):

List<String> result = names.stream().map(String::toUpperCase).toList();

sorted, distinct и limit

Эти промежуточные операции перестраивают стрим перед сбором:

sorted() без аргумента использует естественный порядок; передайте Comparator (здесь лямбду) для своего порядка. Comparator.reverseOrder() - более чистый способ сортировки по убыванию.

reduce: свернуть стрим в одно значение

Когда нужно объединить все элементы в один результат - сумму, произведение, самую длинную строку - reduce это универсальный инструмент. Вы передаёте ему начальное значение и функцию, которая объединяет два значения:

Для обычных сумм и средних значений специализированные стримы понятнее: nums.stream().mapToInt(Integer::intValue).sum(). Прибегайте к reduce, когда готового агрегатора нет.

Стримы не заменяют каждый цикл

Стримы хороши, когда вы преобразуете коллекцию в результат. Они не быстрее цикла автоматически и становятся неуклюжими, когда нужно менять внешнее состояние или замысловато прерывать выполнение. Хорошее правило: если конвейер читается как одно ясное предложение - используйте стрим; если вы тянетесь к общему счётчику или к index, обычный цикл for честнее и вполне уместен.

Помните также, что стрим одноразовый. Это выбрасывает исключение:

Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count();   // IllegalStateException: stream has already been operated upon

Дальше: Optional

Несколько терминальных операций стрима - findFirst, min, max, reduce без значения-тождества - могут ничего не найти, поэтому они не возвращают «голое» значение. Они возвращают Optional - контейнер Java для «возможно значение, возможно ничего», который наконец даёт чистую альтернативу возврату null. Это следующая страница.

Часто задаваемые вопросы

Что такое стрим в Java?

Стрим - это конвейер для обработки последовательности элементов (обычно полученных из коллекции) через цепочку операций вроде filter, map и sorted, которая заканчивается терминальной операцией, например collect или count. Он не хранит данные и не изменяет источник; он описывает вычисление. Стрим создаётся с помощью list.stream(), а цепочку читают сверху вниз, как рецепт.

Как преобразовать стрим обратно в список в Java?

Завершите конвейер терминальным коллектором: list.stream().filter(...).collect(Collectors.toList()). В Java 16+ можно использовать более короткий .toList(), который возвращает неизменяемый список. Без терминальной операции вообще ничего не выполнится, потому что промежуточные операции вроде filter и map ленивые.

Когда стоит использовать стрим вместо цикла for?

Берите стрим, когда вы преобразуете или фильтруете коллекцию, получая результат - такой конвейер читается как одно ясное предложение ("взять имена, оставить длинные, перевести в верхний регистр, собрать в список"). Оставайтесь на обычном цикле for, когда нужно изменять внешнее состояние, сложным образом прерывать выполнение или когда логика проще в виде императивных шагов. Стримы про ясность, а не про чистую скорость.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ