Stream API (Java 8+) – механизм для функциональной обработки коллекций и массивов: позволяет применить последовательность операций над данными (отбор, преобразование, агрегирование) в декларативном стиле.
Стрим (Stream) – не хранит данные, а проходит по источнику данных (коллекция, массив, генератор и т.д.), выполняя промежуточные операции (filter, map, sorted, и др.) и завершающую операцию (forEach, collect, reduce...).
Основные операции: filter (фильтрация по условию), map (преобразование элементов), sorted (сортировка), терминальные: forEach (действие для каждого элемента), collect (сбор результата в коллекцию или другую структуру), reduce (свертка/агрегация результата).
Преимущества: лаконичный и читабельный код для массовых операций над данными; возможность простой параллелизации обработки (parallelStream()).
Пояснение:
Stream API предоставляет новую парадигму работы с данными в Java – подход, вдохновленный функциональным программированием. Вместо явных циклов, в которых мы поэлементно обрабатываем коллекцию, стримы позволяют задавать что сделать с данными, а не как.
Стрим (java.util.stream.Stream) можно получить из коллекции вызовом метода stream() (или parallelStream() для параллельного стрима). Также существуют методы для массивов (Arrays.stream(array)) и фабричные методы в самом API (Stream.of(...)). После получения стрима можно цепочкой вызвать несколько промежуточных операций (intermediate operations), каждая из которых возвращает новый модифицированный стрим (например, отфильтрованный или трансформированный). Завершается цепочка терминальной операцией (terminal operation), которая возвращает конкретный результат или побочный эффект – например, собирает данные в коллекцию или сводит их к одному значению.
Важно отметить: стримы не хранят данные сами по себе. Они лениво обрабатывают элементы из исходной коллекции или другого источника. Промежуточные операции выполняются только когда запускается терминальная операция, и проходят по данным только один раз (в одном потоке либо в нескольких, если стрим параллельный).
Рассмотрим пример использования Stream API. Пусть у нас есть список чисел, и мы хотим из них получить список квадратов четных чис, отсортированный по убыванию:
List<Integer> numbers = List.of(5, 2, 9, 4, 7, 1, 8);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // оставить только четные: [2, 4, 8]
.map(n -> n * n) // преобразовать: возвести в квадрат -> [4, 16, 64]
.sorted(Comparator.reverseOrder()) // сортировать по убыванию -> [64, 16, 4]
.toList(); // собрать в список (Java 16+: аналог collect(Collectors.toList()))
System.out.println(result); // [64, 16, 4]
Шаг за шагом: мы взяли исходный список, отфильтровали (filter) только четные числа, затем преобразовали (map) каждое число в его квадрат, отсортировали результат в обратном порядке и собрали в новый список. Весь этот конвейер описан декларативно, без явных индексов, счетчиков, мутаций коллекций. Код короткий и фокусируется на самой задаче, а не на деталях реализации цикла.
Основные виды операций стрима:
Filter (фильтрация): оставляет только те элементы, которые удовлетворяют условию (предикату). В примере выше n -> n % 2 == 0 – лямбда, фильтрующая четные числа.
Map (отображение, преобразование): применяет функцию к каждому элементу, возвращая элементы нового типа или измененные элементы. Например, преобразование объекта в один из его полей, или вычисление квадратного значения числа, как мы сделали.
Sorted (сортировка): упорядочивает элементы. Без параметров – по естественному порядку, с параметром Comparator – по заданному правилу. Сортировку лучше делать ближе к концу конвейера, после фильтраций и преобразований, чтобы сортировать уже конечный набор данных.
forEach: терминальная операция, перебирающая все элементы и выполняющая указанное действие для каждого (например, вывод на экран). Используется, когда нужно произвести побочный эффект над каждым элементом (но не для формирования нового списка, т.к. forEach ничего не возвращает).
collect: терминальная операция, собирающая результаты в контейнер (коллекцию, массив, строку и т.д.) или выполняющая агрегирование через Collectors. Очень гибкий механизм – например, collect(Collectors.toList()) собирает все элементы стрима в список, Collectors.toSet() – в множество, Collectors.joining(", ") – объединит элементы (например, строки) в одну строку через разделитель, и многое другое. В Java 16 для простых случаев появились методы toList()/toSet() прямо у стрима.
reduce (свертка): терминальная операция, которая агрегирует все элементы в одно значение, используя бинарную операцию. Например, суммирование всех чисел: int sum = numbers.stream().reduce(0, Integer::sum); – здесь 0 начальное значение, а Integer::sum берет два числа и возвращает их сумму, в итоге получается сумма всех элементов.
Параллельные стримы: Одно из преимуществ Stream API – возможность легко распараллелить вычисления. Вызов collection.parallelStream() вместо stream() разбивает работу стрима между несколькими потоками ForkJoinPool. Для вычислительно интенсивных задач на большом количестве данных это может дать выигрыш в производительности на многоядерных процессорах. Однако параллельные стримы нужно использовать с осторожностью: не все операции масштабируются, и накладные расходы на синхронизацию могут превысить выигрыш. Кроме того, в параллельном режиме операции с побочными эффектами (например, forEach, который пишет в общую переменную) могут работать непредсказуемо, поэтому рекомендуется по возможности использовать неизменяемые операции и чистые функции внутри стримов.
Современная практика: Stream API широко используется для обработки данных в Java. Он особенно удобен, когда требуется выполнить несколько последовательных преобразований коллекции. Код со стримами часто компактнее и выразительнее, чем эквивалентный код с циклами. Однако для очень простых случаев (например, один проход без сложных преобразований) классический цикл for или for-each может быть понятнее. Важно уметь применять оба подхода.