Stream API

Stream — одноразовый конвейер вычислений над последовательностью элементов. Он не хранит данные, а получает их из источника: коллекции, массива, генератора, файла или другого stream.

Структура конвейера

источник → промежуточные операции → терминальная операция
List<String> result = users.stream()
        .filter(User::active)
        .map(User::name)
        .filter(name -> !name.isBlank())
        .sorted()
        .toList();

Промежуточные операции возвращают новый stream и обычно ленивы. Работа начинается после терминальной операции. Элементы часто проходят конвейер по одному: filter может сразу передать элемент в map, не создавая промежуточной коллекции.

Подробнее: Промежуточные операции и Терминальные операции.

Stream и Collection

CollectionStream
хранит элементыописывает вычисление
можно обходить повторнопосле терминальной операции закрыт
допускает изменение содержимогооперации не должны менять источник
eager API хранениябольшая часть pipeline ленива

Повторное использование заканчивается IllegalStateException:

Stream<String> stream = names.stream();
long count = stream.count();
// stream.findFirst(); // stream has already been operated upon or closed

Создание

Stream.of("a", "b");
Arrays.stream(values);
collection.stream();
IntStream.range(0, 10);
Stream.iterate(1, n -> n <= 100, n -> n * 2);
Stream.generate(UUID::randomUUID).limit(5);

Для IntStream, LongStream, DoubleStream есть числовые операции без boxing каждого элемента. Переходы: mapToInt, boxed.

Побочные эффекты

Функции pipeline должны быть stateless и non-interfering: не зависеть от изменяемого общего состояния и не менять источник во время обхода.

// Плохо: общий mutable accumulator
List<String> output = new ArrayList<>();
names.parallelStream().forEach(output::add);
 
// Результат выражен самой операцией
List<String> output = names.parallelStream().map(String::trim).toList();

Parallel stream

parallel() использует разбиение источника и обычно общий ForkJoinPool.commonPool. Параллельность выгодна не автоматически: мешают малый объём, дешёвые операции, плохое разбиение, блокирующий I/O и общий state. Особенно важны ассоциативность reduce и корректный collector.

Ресурсные stream (Files.lines и подобные) закрывают через try-with-resources. Для обычного collection stream закрытие ничего не делает.

Связанные темы: Iterable, Comparator Java, Обобщения generics.

Вопросы для собеседования

  1. Хранит ли stream элементы? Нет, он потребляет источник.
  2. Когда выполняется filter? При запуске терминальной операции.
  3. Можно ли использовать stream дважды? Нет.
  4. Чем map отличается от flatMap? map даёт один результат на элемент, flatMap разворачивает вложенный stream.
  5. Почему побочные эффекты опасны? Они ломают независимость элементов и особенно заметны при parallel.
  6. Всегда ли parallel быстрее? Нет, накладные расходы могут быть больше работы.
  7. Зачем primitive streams? Чтобы выполнять числовые операции без лишнего boxing.