Stream API ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-14

Что такое Stream API в Java

Этот код компилируется, но падает в рантайме:

List<String> words = List.of("hello", "hola", "hallo");
Stream<String> stream = words.stream();

stream.forEach(System.out::println); // всё хорошо
long count = stream.count();         // java.lang.IllegalStateException:
                                     // stream has already been operated upon or closed

Причина не в опечатке: поток в Java одноразовый. Понимание того, чем Stream отличается от коллекции, снимает большинство таких вопросов сразу — и с этого мы начнём.

1. Что такое Stream API в Java

Stream API — это набор классов и интерфейсов пакета java.util.stream, появившийся в Java 8, который позволяет обрабатывать последовательности элементов в декларативном (функциональном) стиле. Stream (поток) — это последовательность элементов из источника данных (коллекции, массива, файла, генератора), поддерживающая последовательные и параллельные операции обработки.

Ключевая идея: вы описываете, что нужно сделать с данными (отфильтровать, преобразовать, собрать), а не как это сделать через циклы и временные переменные. Сам поток при этом не является структурой данных — он не хранит элементы, а лишь пропускает их через цепочку операций.

Важно не путать java.util.stream.Stream с потоками ввода-вывода (InputStream, OutputStream) и с потоками выполнения (Thread). Это три разные вещи, которые по-русски называются одним словом «поток».

2. Зачем нужен Stream API

  • Меньше шаблонного кода. Фильтрация, сортировка и сбор результата умещаются в одну цепочку вместо десятка строк с циклом и промежуточным списком.
  • Читаемость. Цепочка filter().map().collect() читается как предложение и описывает намерение, а не механику обхода.
  • Параллельная обработка из коробки. Один вызов parallelStream() вместо ручной работы с ExecutorService.
  • Богатый набор операций. Фильтрация, преобразование, сортировка, группировка, агрегация, поиск — всё в стандартной библиотеке.
  • Ленивость. Промежуточные операции не выполняются, пока не запрошен результат, поэтому лишняя работа не делается.

3. Ключевые свойства потоков

  1. Поток не хранит данные. Он получает элементы из источника и передаёт их дальше по цепочке. Источник живёт своей жизнью.
  2. Поток не изменяет источник. sorted(), filter(), map() возвращают новый поток, а исходная коллекция остаётся нетронутой (в отличие от Collections.sort(), который сортирует список на месте).
  3. Поток одноразовый. После терминальной операции он закрыт; повторное обращение бросает IllegalStateException.
  4. Операции ленивые. Промежуточные операции только строят конвейер; реальная обработка стартует лишь при вызове терминальной операции.
  5. Нет доступа по индексу. У потока нет get(i) — если нужен произвольный доступ, работайте со списком или массивом.
  6. Тесная интеграция с лямбда-выражениями и ссылками на методы: почти каждый метод Stream принимает функциональный интерфейс.

Важно

Цепочка без терминальной операции не выполняет вообще ничего. Вызов list.stream().filter(s -> { System.out.println(s); return true; }); не напечатает ни строки: лямбда внутри filter сработает только тогда, когда кто-то запросит результат.

4. Интерфейсы: Stream, IntStream, LongStream, DoubleStream

Stream API строится вокруг четырёх интерфейсов:

  • java.util.stream.Stream<T> — поток объектов любого ссылочного типа.
  • IntStream, LongStream, DoubleStream — специализированные потоки примитивов.

Специализированные потоки существуют не ради красоты: Stream<Integer> вынужден упаковывать каждое число в объект (автобоксинг), а IntStream работает с примитивами напрямую. Кроме того, у примитивных потоков есть методы, которых нет у Stream<T>: sum(), average(), summaryStatistics().

// Stream<Integer>: боксинг на каждом элементе, метода sum() нет
int total1 = List.of(1, 2, 3).stream().mapToInt(Integer::intValue).sum();

// IntStream: примитивы, sum() есть
int total2 = IntStream.of(1, 2, 3).sum();               // 6
double avg = IntStream.of(1, 2, 3).average().orElse(0); // 2.0

Переходы между ними: mapToInt(), mapToLong(), mapToDouble() — из объектного потока в примитивный; boxed() или mapToObj() — обратно.

Полный список методов см. в официальной документации пакета java.util.stream.

5. Как создать Stream: 7 способов

1. Из коллекции — самый частый вариант, метод stream() есть у любой Collection:

List<String> words = Arrays.asList("hello", "hola", "hallo", "ciao");
Stream<String> stream = words.stream();

2. Из перечисленных значенийStream.of():

Stream<String> stream = Stream.of("hello", "hola", "hallo", "ciao");

3. Из массива объектов:

String[] words = {"hello", "hola", "hallo", "ciao"};
Stream<String> stream = Arrays.stream(words); // или Stream.of(words)

4. Из массива примитивов — получится IntStream, а не Stream<Integer>:

int[] nums = {1, 2, 3, 4, 5};
System.out.println(Arrays.stream(nums).count()); // 5
int[] nums = {1, 2, 3, 4, 5};
System.out.println(IntStream.of(nums).count()); // 5

5. Генератором и итератором — такие потоки бесконечны, поэтому им нужен ограничитель:

Stream<Double> random = Stream.generate(Math::random).limit(5);
Stream<Integer> powers = Stream.iterate(1, n -> n * 2).limit(5); // 1, 2, 4, 8, 16

// Java 9+: iterate с условием остановки, limit не нужен
Stream<Integer> upTo100 = Stream.iterate(1, n -> n < 100, n -> n * 2);

6. Через билдер:

Stream<String> s = Stream.<String>builder()
        .add("h").add("e").add("l").add("l").add("o")
        .build();
s.forEach(System.out::print); // hello

7. Диапазоном чисел:

IntStream s1 = IntStream.range(1, 4);       // 1, 2, 3 - верхняя граница не входит
IntStream s2 = IntStream.rangeClosed(1, 4); // 1, 2, 3, 4 - включительно

Есть и другие источники: Files.lines(path) для строк файла, String.chars() для символов, Random.ints(), Pattern.splitAsStream(), Stream.empty() для пустого потока.

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

Любой конвейер Stream состоит из трёх частей: источник → промежуточные операции → терминальная операция. Разница между двумя типами операций принципиальная:

  • Промежуточная операция (например, filter, map, sorted, distinct, limit) возвращает новый Stream, ничего не вычисляя. Их можно цеплять сколько угодно.
  • Терминальная операция (например, collect, forEach, count, reduce) возвращает результат (значение, коллекцию, Optional) или void, запускает всю цепочку и закрывает поток.

Ленивость экономит работу: в примере ниже map отработает не для всех элементов, а ровно до первого совпадения.

Optional<String> first = Stream.of("apple", "banana", "avocado", "cherry")
        .map(String::toUpperCase)   // выполнится только для "apple"
        .filter(s -> s.startsWith("A"))
        .findFirst();               // терминальная операция, обход прерывается
System.out.println(first.orElse("не найдено")); // APPLE

Полный список промежуточных и терминальных методов с таблицами и примерами (включая reduce(), toArray(), iterator()/spliterator(), разницу stateless/stateful) разобран в отдельном уроке «Промежуточные и оконечные операции Stream».

7. Примеры обработки коллекций

Задача: выбрать студентов с баллом выше 90 и отсортировать их по баллу.

Как это писали до Java 8:

List<Student> studentsScore = new ArrayList<>();
for (Student s : students) {
    if (s.getScore() > 90.0) {
        studentsScore.add(s);
    }
}
studentsScore.sort(new Comparator<Student>() {
    public int compare(Student s1, Student s2) {
        return Double.compare(s1.getScore(), s2.getScore());
    }
});

То же самое через Stream API — одна цепочка вместо цикла, временного списка и анонимного класса:

List<Student> studentsScore = students.stream()
        .filter(s -> s.getScore() > 90.0)
        .sorted(Comparator.comparing(Student::getScore))
        .toList(); // Java 16+; раньше: .collect(Collectors.toList())

Ещё несколько типовых цепочек:

// Имена в верхнем регистре через запятую
String names = students.stream()
        .map(Student::getName)
        .map(String::toUpperCase)
        .collect(Collectors.joining(", "));

// Средний балл
double average = students.stream()
        .mapToDouble(Student::getScore)
        .average()
        .orElse(0.0);

// Группировка по городу
Map<String, List<Student>> byCity = students.stream()
        .collect(Collectors.groupingBy(Student::getCity));

// Есть ли хоть один отличник
boolean hasExcellent = students.stream().anyMatch(s -> s.getScore() > 95.0);

Совет

Начиная с Java 16 вместо collect(Collectors.toList()) можно писать просто toList(). Разница не только в длине: Stream.toList() возвращает неизменяемый список, а Collectors.toList() не гарантирует ни изменяемость, ни конкретный класс результата. Если список потом нужно менять, берите collect(Collectors.toCollection(ArrayList::new)).

8. На чём чаще всего спотыкаются

  1. Повторное использование потока. Ошибка из начала урока: после терминальной операции поток закрыт. Если нужен второй проход, создавайте поток заново или храните Supplier<Stream<T>>:
    Supplier<Stream<String>> supplier = words::stream;
    supplier.get().forEach(System.out::println);
    long count = supplier.get().count(); // работает
  2. Забытая терминальная операция. Цепочка из одних промежуточных операций не делает ничего и не выдаёт ошибку — код просто молча не выполняется.
  3. Stream.of() с массивом примитивов. Stream.of(new int[]{1, 2, 3}) даёт Stream<int[]> из одного элемента, а не поток чисел. Для примитивов используйте Arrays.stream(nums) или IntStream.of(nums).
  4. Бесконечный поток без ограничителя. Stream.generate(Math::random).forEach(...) будет крутиться вечно. Потоки от generate() и двухаргументного iterate() обязательно ограничивают через limit() или takeWhile().
  5. Изменение источника во время обхода. Добавление или удаление элементов коллекции внутри лямбды приводит к ConcurrentModificationException.
  6. Побочные эффекты вместо сбора результата. Вместо forEach(result::add) используйте collect() или toList() — это безопаснее, особенно в параллельных потоках.

Обратите внимание

parallelStream() — не бесплатное ускорение. Все параллельные потоки по умолчанию делят общий ForkJoinPool.commonPool(), а на маленьких коллекциях накладные расходы на разбиение и слияние съедают выигрыш. Параллелизм оправдан на больших объёмах данных и независимых операциях без побочных эффектов — и только после замеров.

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

Чем Stream отличается от коллекции?

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

Почему код внутри filter или map не выполняется?

Промежуточные операции ленивые: они только описывают, что нужно сделать. Пока в цепочке нет терминальной операции — collect, forEach, count, reduce, findFirst и подобных — ни одна лямбда не будет вызвана. Добавьте терминальную операцию, и конвейер запустится.

Чем IntStream отличается от Stream<Integer>?

IntStream работает с примитивами int напрямую, без упаковки в объекты, поэтому создаёт меньше нагрузки на память и сборщик мусора. У него есть числовые методы sum, average, max, min и summaryStatistics, которых нет у объектного Stream. Перейти из объектного потока в примитивный можно через mapToInt, обратно — через boxed.

Как из Stream получить List?

В Java 16 и новее проще всего вызвать toList() — он вернёт неизменяемый список. В более старых версиях используется collect(Collectors.toList()). Если результат нужно потом менять, соберите его через collect(Collectors.toCollection(ArrayList::new)).

Всегда ли parallelStream быстрее обычного потока?

Нет. Параллельный поток разбивает данные на части, обрабатывает их в общем пуле ForkJoinPool и объединяет результаты — на небольших коллекциях эти накладные расходы больше самой работы. Выигрыш появляется на больших объёмах данных, при независимых операциях без побочных эффектов и хорошо разбиваемом источнике вроде ArrayList или массива. Решение принимается по результатам замеров, а не по умолчанию.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.