Stream Gatherers in Java
A stream gatherer in Java is a way to create your own intermediate operation for a stream, just as a collector lets you create your own terminal operation. Gatherers were added as a final feature in Java 24 (JEP 485). You use one with the new gather method, for example stream.gather(Gatherers.windowFixed(3)). The java.util.stream.Gatherers class provides several ready-made gatherers, such as grouping elements into windows or computing running totals. In this lesson, you will learn the built-in gatherers and how to write a simple one yourself.
Why Gatherers Were Added
Before Java 24, the set of intermediate operations was fixed: filter, map, flatMap, distinct, sorted, limit and a few more. Tasks such as "split the stream into groups of three", "running total" or "keep an element only if it differs from the previous one" had no clean stream solution. Gatherers let any such operation be written once and used like a built-in one.
Built-In Gatherers
| Method | Description |
|---|---|
Gatherers.windowFixed(n) | It groups elements into lists of n (the last may be shorter). |
Gatherers.windowSliding(n) | It produces overlapping lists of n, moving one element at a time. |
Gatherers.fold(init, folder) | It combines all elements into one result, emitted at the end. |
Gatherers.scan(init, scanner) | It emits a running result after each element. |
Gatherers.mapConcurrent(max, mapper) | It maps elements concurrently using virtual threads, keeping order. |
Example: Windows
Let's see an example that groups marks into windows.
import java.util.List;
import java.util.stream.Gatherers;
public class Windows {
public static void main(String[] args) {
List<Integer> marks = List.of(91, 78, 85, 66, 72, 88, 95);
System.out.println(marks.stream()
.gather(Gatherers.windowFixed(3))
.toList());
System.out.println(marks.stream()
.gather(Gatherers.windowSliding(3))
.map(w -> w.stream().mapToInt(Integer::intValue).average()
.orElse(0))
.map(avg -> Math.round(avg * 10) / 10.0)
.toList());
}
}[[91, 78, 85], [66, 72, 88], [95]]
[84.7, 76.3, 74.3, 75.3, 85.0]Explanation: windowFixed(3) splits the marks into groups of three; the last group has only one mark left. windowSliding(3) produces overlapping groups: marks 1 to 3, then 2 to 4, and so on. The second pipeline turns each window into its average, giving a moving average, rounded to one decimal place.
Example: Running Totals with scan, and fold
import java.util.List;
import java.util.stream.Gatherers;
public class ScanFold {
public static void main(String[] args) {
List<Integer> deposits = List.of(500, 200, 300, 1000);
System.out.println(deposits.stream()
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList());
System.out.println(deposits.stream()
.gather(Gatherers.fold(() -> 0, Math::max))
.toList());
}
}[500, 700, 1000, 2000]
[1000]Explanation: scan emits the running balance after every deposit. fold combines everything into one result and emits it only at the end; here it finds the largest deposit. Unlike reduce, these are intermediate operations, so more steps can follow.
Example: Writing Your Own Gatherer
A gatherer is built with Gatherer.ofSequential, which takes an optional initializer (for state) and an integrator that receives each element and can push zero or more results downstream:
import java.util.List;
import java.util.Objects;
import java.util.stream.Gatherer;
public class Dedupe {
static <T> Gatherer<T, ?, T> dropRepeats() {
class Last { T value; boolean seen; }
return Gatherer.ofSequential(
Last::new,
(state, element, downstream) -> {
if (state.seen && Objects.equals(state.value, element)) {
return true;
}
state.value = element;
state.seen = true;
return downstream.push(element);
});
}
public static void main(String[] args) {
List<String> log = List.of("Navika", "Navika", "Srihitha",
"Srihitha", "Srihitha", "Navika");
System.out.println(log.stream().gather(dropRepeats()).toList());
}
}[Navika, Srihitha, Navika]Explanation: The state remembers the last element. For each new element, the integrator skips it if it equals the previous one; otherwise it stores it and pushes it downstream. Returning true means "keep sending me elements". Unlike distinct, this removes only consecutive repeats, so Navika appears again at the end.
Common Mistakes
Trying gather on an older JDK. It needs Java 24 or newer.
Using fold when a terminal reduce or collector would do. fold is useful when more intermediate steps must follow.
Forgetting to return the result of downstream.push. It tells the stream whether to keep going (for example, after a limit).
Practice
- Easy: Use
windowFixed(2)to pair up a list of names into teams of two. - Harder: Write a gatherer
everyNth(n)that passes on only every n-th element, and test it onIntStream.rangeClosed(1, 20).boxed().
Key Takeaways
Was this tutorial helpful?