A Gentle Introduction to Java 25 Stream Gatherers
By Ercan - 01/11/2025
Since Java 8, the Stream API has revolutionized how we process collections and sequences in Java. With map, filter and flatMap, developers gained a declarative, functional approach to transforming data. However, as real-world data processing evolved, the need for stateful intermediate operations, windowed processing, and concurrent mapping became evident.
Java 25 addresses these gaps by introducing Gatherers, a new set of tools in the Stream API designed to simplify complex stream workflows. Whether you want to perform sliding window calculations, cumulative scans, or batch processing with concurrency, Gatherers provide a clean, expressive way to handle it.
What Are Gatherers?
A Gatherer is essentially an advanced intermediate operation for streams. It allows you to:
- Maintain state across elements.
 - Produce many-to-one or many-to-many transformations.
 - Implement windowing, folding, scanning, and concurrent mapping.
 - Compose custom operations for specialized pipelines.
 
The core components of a Gatherer are:
- Initializer – prepares an intermediate container or state.
 - Integrator – incorporates each stream element into the current state.
 - Combiner – merges states when the stream is parallelized.
 - Finisher – transforms the accumulated state into the final output element(s).
 
Streams now gain a new method: stream.gather(Gatherer), enabling these advanced operations in the middle of a stream pipeline.
Why We Needed Gatherers
Before Gatherers, implementing a sliding window or a cumulative scan often required:
- Manual state tracking using 
AtomicReferenceor mutable containers. - Intermediate collection accumulation with 
Collectors.toList(). - Breaking the fluent chain of stream operations for post-processing.
 
Gatherers solve this by integrating stateful logic directly into the stream, maintaining readability, and enabling seamless parallel execution.
Built-in Gatherers in Java 25
List<Integer> numbers = IntStream.rangeClosed(1, 15)
                                 .boxed()
                                 .collect(Collectors.toList());
1. windowFixed(int size)
Purpose: Groups stream elements into fixed-size lists.
Example Use Case: Batch database inserts, chunked processing of API calls.
List<List<Integer>> batches = numbers.stream()
                                     .gather(Gatherers.windowFixed(5))
                                     .toList();
batches.forEach(System.out::println);
Output:
[1, 2, 3, 4, 5] [6, 7, 8, 9, 10] [11, 12, 13, 14, 15]
Why it’s useful: Eliminates boilerplate loops for batching, keeps the stream pipeline intact, and works seamlessly with parallel streams.
2. windowSliding(int size)
Purpose: Produces overlapping sublists of the given size.
Example Use Case: Calculating moving averages or trend detection in time-series data.
List<Integer> slidingWindows = numbers.stream()
                                      .gather(Gatherers.windowSliding(3))
                                      .map(window -> window.stream().mapToInt(Integer::intValue).sum())
                                      .toList();
System.out.println(slidingWindows);
Output:
[6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42]
Why it’s useful: Sliding windows previously required cumbersome mutable lists or external libraries. Gatherers integrate it directly into the stream pipeline.
3. fold(Supplier<R> initial, BiFunction<? super R,? super T,? extends R> folder)
Purpose: Accumulates elements into a single result while remaining an intermediate operation.
Example Use Case: Compute cumulative totals or combine elements into a single structure before further processing.
var cumulativeProduct = numbers.stream()
                               .gather(Gatherers.fold(() -> 1, (acc, n) -> acc * n))
                               .toList();
System.out.println(cumulativeProduct);
Why it’s useful: Unlike reduce, fold as an intermediate op allows downstream operations on the accumulated results without terminating the stream.
4. scan(Supplier<R> initial, BiFunction<? super R,? super T,? extends R> scanner)
Purpose: Returns each intermediate accumulation.
Example Use Case: Generate cumulative totals, track progress over a sequence of numbers.
var cumulativeSum = numbers.stream()
                           .gather(Gatherers.scan(() -> 0, Integer::sum))
                           .toList();
System.out.println(cumulativeSum);
Output:
[1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120]
Why it’s useful: Previously required custom collectors or external state management. Now integrated into the stream seamlessly.
5. mapConcurrent(int maxConcurrency, Function<? super T,? extends R> mapper)
Purpose: Apply a function concurrently on stream elements.
Example Use Case: Execute multiple I/O-bound operations in parallel using virtual threads.
var urls = List.of("https://example.com/1", "https://example.com/2");
var responses = urls.stream()
                    .gather(Gatherers.mapConcurrent(2, url -> fetchUrl(url)))
                    .toList();
System.out.println(responses);
Why it’s useful: Reduces the need to manually manage thread pools for parallel mapping; integrates naturally into the stream API.
When to Use Gatherers
- Use cases:
	
- Sliding window calculations for analytics
 - Batching for DB/API operations
 - Cumulative or prefix scans
 - Parallel processing with bounded concurrency
 
 - Avoid when: Simple map/filter operations suffice; overuse can reduce readability
 
Performance Notes & Best Practices
- Parallel streams: Always define proper combiner for safe parallel execution.
 - Memory usage: Large windows or scans may hold many elements; choose window size carefully.
 - Readability: Use built-in gatherers when they simplify your pipeline; custom gatherers should be well-documented.
 
Conclusion
Java 25’s Gatherers bring a powerful stateful dimension to the Stream API. They make operations like sliding windows, cumulative scans, and concurrent mapping readable, maintainable and parallel-friendly. By leveraging both built-in and custom gatherers, developers can simplify complex stream workflows, keeping the code concise and expressive.
Gatherers are not just a feature—they are a paradigm shift in how Java developers can handle intermediate stream transformations.
