🌊 Streams API · Intermediate

Stream gatherers in Java

Java 24 gather() with windowFixed, windowSliding, fold, scan.

🧩 The mysterySplit a stream into pairs. Compute a running total. Try it with only map, filter and reduce… you can't, cleanly. Java 24 finally added the missing piece.

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]]
🔮 Predict it

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. [[1, 2, 3], [4, 5, 6]]
  2. [[1, 2, 3], [4, 5, 6], [7]]
  3. [[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]]
🔮 Predict it

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);
  1. [0, 5, 10, 15]
  2. [5, 10, 15]
  3. [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.

⚠️ The trap

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]
💼 In the real world

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

  1. stream.gather(gatherer) is an intermediate operation
  2. windowFixed(n): chunks of n; last chunk may be smaller
  3. windowSliding(n): overlapping windows moving by 1
  4. scan emits every running result; fold emits only the final one
🤯 Did you know?

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. [[1, 2], [3, 4]]
  2. [[1, 2], [3, 4], [5]]
  3. [[1, 2], [2, 3], [3, 4], [4, 5]]
  4. [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. [[1, 2, 3], [4]]
  2. [[1, 2, 3], [2, 3, 4]]
  3. [[1, 2, 3], [2, 3, 4], [3, 4], [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.

Next world — the Core APIs toolbox: why does 0.1 + 0.2 print 0.30000000000000004, and how do you count money correctly?