Stream API в Java: ленивый конвейер, завершение и повторный запуск
Автор: Казачкин Даниил Михайлович · Обновлено
Stream API описывает преобразование последовательности элементов: откуда их получить, какие оставить, во что превратить и как завершить вычисление. Stream не хранит коллекцию внутри себя и не означает отдельный поток выполнения.
Источник, преобразования, результат
Конвейер состоит из источника, промежуточных операций и завершающей операции. filter сохраняет элементы, удовлетворяющие условию; map преобразует каждый прошедший элемент. Запись n -> n * 10 — lambda: на вход получает n, возвращает его произведение на 10. Это функция для вызова библиотекой, а не немедленно вычисленное число. Модель map/filter/reduce.
Промежуточные операции ленивы: описание фильтра не начинает обход. Завершение через toList, sum, findFirst или другую терминальную операцию запускает необходимую обработку. На объём работы влияет результат: поиск первого совпадения может остановиться раньше конца. Промежуточные операции.
Проследите один конечный проход
Файл LazyPipeline.java специально печатает из предиката, чтобы наблюдать последовательный учебный пример:
import java.util.List;
import java.util.stream.Stream;
public class LazyPipeline {
public static void main(String[] args) {
List<Integer> input = List.of(1, 2, 3, 4);
Stream<Integer> pipeline = input.stream()
.filter(n -> {
System.out.println("check " + n);
return n % 2 == 0;
})
.map(n -> n * 10);
System.out.println("prepared");
System.out.println(pipeline.toList());
System.out.println(input);
try {
pipeline.count();
} catch (IllegalStateException expected) {
System.out.println("stream already used");
}
}
}Команды: javac -encoding UTF-8 --release 25 LazyPipeline.java, java LazyPipeline. Результат:
prepared
check 1
check 2
check 3
check 4
[20, 40]
[1, 2, 3, 4]
stream already usedСтрока prepared появляется до обхода. Исходный список не изменился, потому что программа создала новые числовые значения и собрала их отдельно. Один stream используется один раз; для нового вычисления создайте новый input.stream(). Переиспользовать коллекцию можно, уже потреблённый конвейер — нельзя. Контракт Stream.
Что действительно завершается
forEach завершает stream и возвращает void, поэтому после него нельзя продолжить цепочку filter. count возвращает long, findFirst — Optional<T>, а mapToInt(...).sum() — число. Optional позволяет выразить, что совпадения может не быть: вызывайте orElse с осмысленным значением или обрабатывайте отсутствие отдельно. Терминальные операции.
Stream.toList() возвращает немодифицируемый список. Если следующему этапу нужен рабочий ArrayList, используйте collect(Collectors.toCollection(ArrayList::new)). Двоеточие :: в такой записи передаёт ссылку на конструктор; библиотека создаёт контейнер, когда он требуется. Тип результата нельзя угадывать по тому, что одна версия Collectors.toList() сегодня вернула ArrayList.
Побочные эффекты нельзя превращать в протокол
Диагностические сообщения из примера объясняют конкретный проход, но не должны отвечать за сохранение данных. Реализация вправе не выполнять ненужные стадии, когда результат терминальной операции можно получить иначе. Например, вывод из peek перед count не является надёжным способом посчитать выполненные действия. Нужное сохранение оформляйте явным этапом с обработкой отказов, а преобразования делайте зависимыми только от входного элемента.
Не меняйте исходную коллекцию во время её обработки и не складывайте результат параллельного stream во внешний ArrayList. Это создаёт взаимодействие между стадиями и потоками, которое простой на вид pipeline скрывает. parallelStream также не обещает ускорения: разделение работы и объединение результатов имеют цену. Ограничения параллельной обработки.
Как проверить, что вы поняли ленивость
В отдельной копии программы замените toList() на findFirst(). Для заданного источника достаточно проверить 1 и 2, чтобы получить первое преобразованное чётное значение. Затем добавьте .limit(1) перед toList() и сравните результат. Сохраняйте последовательный режим, иначе порядок диагностических сообщений становится отдельным вопросом.
Попробуйте пустой источник и источник только из нечётных чисел. В первом варианте будет пустой список или пустой Optional, во втором — тот же результат после проверки элементов. Напишите ожидаемый результат до запуска: отсутствие совпадения — нормальный исход, а не исключение.
Частые вопросы
Stream быстрее обычного for?
Это не гарантия API. Его преимущество — понятное описание преобразования и готовые операции. Для требования по времени нужны измерения на реальных данных; короткий последовательный цикл нередко остаётся самым ясным решением.
Почему после count нельзя вызвать toList на том же stream?
count уже завершил конвейер. Создайте новый stream из источника или один раз сохраните нужный результат в коллекцию. Сам объект Stream не предназначен для хранения готовых данных и повторного чтения.