Stream Gatherers: Custom Intermediate Ops for Java Streams

~
~
Published on
Authors
java-24-stream-gatherers-banner

JEP 485: Stream Gatherers — Custom Intermediate Operations for Java Streams (Java 24)

Java 24 introduces Stream Gatherers, a powerful new extension point for the Java Stream API that allows developers to build custom intermediate operations.

For years, developers relied on the fixed set of built-in stream operations like map, filter, and flatMap. If you wanted something more advanced—like sliding windows, prefix sums, or controlled concurrency—you often had to fall back to awkward collectors or manual loops.

With JEP 485, Java finally provides a clean and composable way to create these operations.

Think of it like this:

Stream.gather(Gatherer) is to intermediate operations what collect(Collector) is to terminal operations.

This feature was previewed in JDK 22 (JEP 461) and JDK 23 (JEP 473) and is now final in Java 24.


Why Stream Gatherers Matter

The Java Stream API has always been powerful, but it had a limitation: developers couldn't easily extend the set of intermediate operations.

Before Java 24, solving problems like these was awkward:

  • Sliding window calculations
  • Prefix sums
  • Stateful deduplication
  • Batching or chunking
  • Controlled parallelism

Developers often had to:

  • Use collect() in unnatural ways
  • Materialize intermediate collections
  • Write manual loops instead of streams

Stream Gatherers fix this problem.

They introduce a first-class extension mechanism that lets you create custom intermediate transformations while keeping stream pipelines declarative, composable, and parallel-friendly.


Stream Gatherers in One Minute

A gatherer is used like this:

stream.gather(myGatherer)

A Gatherer<T, A, R> processes elements flowing through the stream pipeline.

Gatherers can:

  • Perform one-to-one transformations
  • Emit multiple outputs per element
  • Combine many inputs into fewer outputs
  • Maintain state across elements
  • Short-circuit a stream
  • Run in parallel when a combiner is provided
  • Compose with other gatherers using andThen

This flexibility makes gatherers a natural extension of the stream model.


Built-In Gatherers in Java 24

Java 24 ships with several built-in gatherers in:

java.util.stream.Gatherers

These cover many common streaming patterns.

fold(initial, folder)

Aggregates values and emits a single result at the end.

Pattern: many → one

scan(initial, scanner)

Produces a running accumulation (prefix sum).

Pattern: one → one

windowFixed(size)

Groups elements into fixed-size batches.

Pattern: many → many

windowSliding(size)

Creates overlapping sliding windows.

Pattern: many → many

mapConcurrent(maxConcurrency, mapper)

Processes elements concurrently while preserving encounter order.

Pattern: one → one


Example 1: Sliding Window Averages

A classic stream problem is calculating sliding windows.

With gatherers, this becomes simple and expressive.

import java.util.stream.*;
import java.util.stream.Gatherers;
import java.util.*;

List<Double> avgs = IntStream.rangeClosed(1, 8).boxed()
    .gather(Gatherers.windowSliding(3))
    .map(win -> win.stream().mapToInt(i -> i).average().orElse(0))
    .toList();

Output:

[2.0, 3.0, 4.0, 5.0, 6.0, 7.0]

Instead of manually tracking window state, the gatherer handles the logic.


Example 2: Prefix Sums with scan

Prefix sums are useful in analytics, finance, and statistics.

var prefix = Stream.of(1,2,3,4)
    .gather(Gatherers.scan(() -> 0, (sum, next) -> sum + next))
    .toList();

Output:

[1, 3, 6, 10]

Each element represents the cumulative sum up to that point.


Example 3: Controlled Concurrency with mapConcurrent

One of the most interesting gatherers is mapConcurrent.

It allows bounded concurrency while preserving stream order.

record User(int id) {}

static String fetchProfile(User u) {
    try { Thread.sleep(50); } catch (InterruptedException ignored) {}
    return "user:" + u.id();
}

var profiles = IntStream.range(1, 10)
    .mapToObj(User::new)
    .gather(Gatherers.mapConcurrent(4, u -> fetchProfile(u)))
    .toList();

Key benefits:

  • Limits concurrency
  • Maintains ordering
  • Ideal for IO-bound workloads

This pairs especially well with virtual threads introduced in recent Java releases.


Example 4: fold for Aggregation

fold allows building a single result from many elements.

String csv = Stream.of(1,2,3,4,5)
    .gather(Gatherers.fold(() -> "", (acc, n) ->
        acc.isEmpty() ? Integer.toString(n) : acc + "," + n))
    .findFirst()
    .orElse("");

Output:

1,2,3,4,5

While similar to reduce, fold works naturally within gatherer pipelines.


Example 5: Composing Gatherers

Gatherers can be combined together using andThen.

var windowsThenScan =
    Gatherers.windowFixed(3).andThen(
        Gatherers.scan(() -> 0, (sum, win) ->
            sum + win.stream().mapToInt(x -> x).sum())
    );

var out = IntStream.rangeClosed(1, 9).boxed()
    .gather(windowsThenScan)
    .toList();

This allows complex stream transformations to remain modular and readable.


Gatherers vs Collectors

Gatherers were intentionally designed to resemble Collectors.

But they serve a different purpose.

FeatureCollectorsGatherers
Pipeline positionTerminalIntermediate
PurposeSummarize resultsTransform elements
State supportYesYes
Parallel supportYesYes
ComposabilityLimitedStrong (andThen)

In short:

  • Collectors finalize streams
  • Gatherers extend streams

When Should You Use Gatherers?

Stream gatherers shine when you need stateful or structured transformations inside a pipeline.

Good use cases include:

  • Sliding window analytics
  • Prefix operations
  • Stateful deduplication
  • Event batching
  • Rate limiting
  • Controlled concurrency
  • Incremental aggregation

They allow you to keep your code functional, composable, and expressive.


FAQ

Is this still a preview feature?

No.

JEP 485 is finalized in Java 24.

Earlier versions required preview flags.


Do I need --enable-preview?

No.

Gatherers are part of the standard API in JDK 24.


Final Thoughts

Stream Gatherers represent one of the most significant upgrades to the Java Stream API since its introduction in Java 8.

By enabling custom intermediate operations, Java 24 gives developers a powerful new tool for building expressive and efficient data pipelines.

Instead of bending streams to fit complex logic, you can now extend the stream model itself.

For developers working with data processing, analytics, concurrent workloads, or functional-style pipelines, gatherers unlock entirely new possibilities.