Stream gatherers in Java
Java 24 gather() with windowFixed, windowSliding, fold, scan.
The missing extension point
collect lets you plug in custom terminal logic. Until recently there was no way to add custom intermediate ops. Gatherers fix that: stream.gather(gatherer) (final in Java 24, JEP 485) is an intermediate operation, so you can keep chaining after it. Ready-made ones live in java.util.stream.Gatherers.
windowFixed: chunks
**Gatherers.windowFixed(n) cuts the stream into lists of n. If elements are left over, the last window is smaller** — nothing is dropped.
var r = Stream.of(1, 2, 3, 4)
.gather(Gatherers.windowFixed(2))
.toList(); // [[1, 2], [3, 4]]Leftovers
What does this print?
var r = Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println(r);[[1, 2, 3], [4, 5, 6]][[1, 2, 3], [4, 5, 6], [7]][[1, 2, 3], [2, 3, 4]]
Show the answer
[[1, 2, 3], [4, 5, 6], [7]] — the leftover 7 forms a final, smaller window.
windowSliding: overlapping views
**Gatherers.windowSliding(n) makes overlapping windows of n that move forward one element at a time**. With 4 elements and n = 3, there are exactly two full windows.
Stream.of(1, 2, 3, 4)
.gather(Gatherers.windowSliding(3))
.toList(); // [[1, 2, 3], [2, 3, 4]]Running totals
scan(init, fn) emits every running result. What prints?
var r = Stream.of(5, 5, 5)
.gather(Gatherers.scan(
() -> 0, Integer::sum))
.toList();
System.out.println(r);[0, 5, 10, 15][5, 10, 15][15]
Show the answer
[5, 10, 15] — scan starts from 0 and emits the running sum after each element. The initial 0 itself is not emitted.
fold is not scan
**fold(init, fn) looks like scan but emits only the final value** — like reduce, but as an intermediate step that yields a one-element stream. Pick scan for every running result, fold for just the end.
Stream.of(1, 2, 3, 4)
.gather(Gatherers.fold(
() -> 0, Integer::sum))
.toList(); // [10]Where gatherers earn their keep
Moving averages over sensor or stock data (windowSliding), batching database inserts 500 rows at a time (windowFixed(500)), running balances in banking statements (scan), and mapConcurrent for calling services concurrently with virtual threads.
Key takeaways
- stream.gather(gatherer) is an intermediate operation
- windowFixed(n): chunks of n; last chunk may be smaller
- windowSliding(n): overlapping windows moving by 1
- scan emits every running result; fold emits only the final one
Gatherers were previewed in Java 22 and 23 before becoming a permanent feature in Java 24 — the JDK's preview process lets real users shake out an API first.
Practice questions
What does this print?
var r = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowFixed(2))
.toList();
System.out.println(r);- [[1, 2], [3, 4]]
- [[1, 2], [3, 4], [5]]
- [[1, 2], [2, 3], [3, 4], [4, 5]]
- [1, 2, 3, 4, 5]
Check your answer
[[1, 2], [3, 4], [5]]. windowFixed(2) cuts the stream into lists of 2. The leftover 5 forms a final, smaller window rather than being dropped.
What does this print?
var r = Stream.of(1, 2, 3, 4)
.gather(Gatherers.windowSliding(3))
.toList();
System.out.println(r);- [[1, 2, 3], [4]]
- [[1, 2, 3], [2, 3, 4]]
- [[1, 2, 3], [2, 3, 4], [3, 4], [4]]
- [[1, 2], [2, 3], [3, 4]]
Check your answer
[[1, 2, 3], [2, 3, 4]]. windowSliding(3) makes overlapping windows that move forward one element at a time. With 4 elements there are exactly two full windows.