Back to Stream Creation & Utilities
Q2Question 2 of 18

Stream Gatherers (JEP 485, Java 24): custom stateful intermediate operations

Key Concepts
  • Gatherers (JEP 485, Java 24) are custom INTERMEDIATE operations, the counterpart to Collectors (terminal)
  • stream.gather(gatherer) enables stateful, one-to-many transformations in the middle of a pipeline
  • Built-in: windowFixed(n) for batching, windowSliding(n) for sliding windows, fold for running accumulation
  • Before Gatherers, stateful intermediate logic required ugly workarounds with reduce or external state
  • A Gatherer has four parts: initializer (state), integrator (process element), combiner (parallel), finisher (emit leftovers)
  • Gatherers complement Collectors: Collectors customize terminal output, Gatherers customize intermediate processing

Definition

Stream Gatherers (JEP 485, Java 24) add custom stateful intermediate operations to the Stream API, enabling windowing, batching, and running accumulation that map/filter/flatMap cannot express.
  • Gatherers are CUSTOM INTERMEDIATE OPERATIONS, the intermediate counterpart to Collectors (which are terminal).
  • stream.gather(Gatherer) inserts a custom stateful transformation into the middle of a pipeline.
  • Built-in gatherers: Gatherers.windowFixed(n) creates fixed-size batches, windowSliding(n) creates overlapping windows, fold accumulates state emitting intermediate results.
  • Why needed: map, filter, and flatMap are stateless. They cannot express "group every 3 elements into a sub-list" or "emit running totals."
  • A Gatherer has four parts: initializer (state), integrator (process element and maybe emit), combiner (parallel), finisher (emit leftovers).
  • Gatherers complement Collectors: Collectors customize HOW you produce the final result, Gatherers customize HOW you process elements in the pipeline.

At National Bank of India, Collectors are like the final packaging at the end of a transaction processing assembly line. Gatherers are like intermediate processing stations: "batch every 3 transactions for bulk settlement" (windowFixed), "look at each 3-transaction window for fraud patterns" (windowSliding), "compute running balance after each transaction" (fold).

Understanding the Concepts

What problem do Gatherers solve?

Before Gatherers, custom stateful intermediate logic (sliding windows, fixed-size batching, running de-duplication) had no clean API. You had to fight with reduce, external mutable state, or collect-then-restream patterns.

How does windowFixed work?

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Gatherers;

public class GathererDemo {
    record Transaction(String id, BigDecimal amount) {}

    public static void main(String[] args) {
        List<Transaction> txns = List.of(
            new Transaction("T001", new BigDecimal("50000")),
            new Transaction("T002", new BigDecimal("75000")),
            new Transaction("T003", new BigDecimal("30000")),
            new Transaction("T004", new BigDecimal("90000")),
            new Transaction("T005", new BigDecimal("45000"))
        );

        // Batch transactions in groups of 2 for bulk settlement
        txns.stream()
            .gather(Gatherers.windowFixed(2))
            .forEach(batch -> System.out.println("Batch: " + batch));
    }
}
TEXT
Batch: [Transaction[id=T001, amount=50000], Transaction[id=T002, amount=75000]]
Batch: [Transaction[id=T003, amount=30000], Transaction[id=T004, amount=90000]]
Batch: [Transaction[id=T005, amount=45000]]

The last batch has only 1 element because 5 is not evenly divisible by 2.

How does windowSliding work?

JAVA
// Sliding window of 3: detect fraud patterns
txns.stream()
    .gather(Gatherers.windowSliding(3))
    .forEach(window -> System.out.println("Window: " + window));
TEXT
Window: [T001, T002, T003]
Window: [T002, T003, T004]
Window: [T003, T004, T005]

Each window overlaps with the previous by n-1 elements.

How does fold work for running totals?

JAVA
// Running balance after each transaction
txns.stream()
    .gather(Gatherers.fold(
        () -> BigDecimal.ZERO,
        (balance, txn) -> balance.add(txn.amount())))
    .forEach(balance -> System.out.println("Running total: Rs." + balance));
TEXT
Running total: Rs.50000
Running total: Rs.125000
Running total: Rs.155000
Running total: Rs.245000
Running total: Rs.290000

Unlike reduce which only returns the final value, fold emits the accumulated value after each element.

How are Gatherers structured?

A custom Gatherer has four parts:

| Part | Purpose | Required |
|------|---------|----------|
| Initializer | Creates mutable state | Yes |
| Integrator | Processes one element, may emit results | Yes |
| Combiner | Merges state for parallel streams | Optional |
| Finisher | Emits remaining state when stream ends | Optional |

How are Gatherers different from Collectors?

| Feature | Gatherers | Collectors |
|---------|-----------|------------|
| Pipeline position | Intermediate | Terminal |
| Input/Output | Stream in, Stream out | Stream in, single result |
| Stateful | Yes | Yes |
| Added in | Java 24 | Java 8 |
| Use case | Transform elements mid-pipeline | Produce final result |

What are common mistakes?

WRONG: Using Gatherers on Java versions before 24.

JAVA
// COMPILE ERROR on Java 21 or earlier:
txns.stream().gather(Gatherers.windowFixed(2));
// Gatherers are not available before Java 24

What should a senior engineer know?

  • Gatherers fill the gap between map/filter (stateless intermediates) and Collectors (stateful terminal). They enable custom stateful intermediate processing.
  • Before Gatherers, sliding windows required IntStream.range(0, list.size() - n + 1).mapToObj(i -> list.subList(i, i + n)), which is clunky and only works on RandomAccess lists.
  • The scan gatherer emits prefix-accumulation at each step, similar to functional programming's scanl.
  • If your interview targets Java 21 or earlier, mention Gatherers as "coming in Java 24" to show awareness of the latest API evolution.

Visual Overview

DIAGRAM
  windowFixed(2):
  [T001, T002, T003, T004, T005]
     │    │     │    │     │
     └────┘     └────┘     │
   Batch 1    Batch 2    Batch 3
  [T001,T002] [T003,T004] [T005]

  windowSliding(3):
  [T001, T002, T003, T004, T005]
   └────────────┘                 Window 1
         └────────────┘           Window 2
                └────────────┘    Window 3

  fold (running total):
  T001(50K)  → 50K
  T002(75K)  → 125K
  T003(30K)  → 155K
  T004(90K)  → 245K
  T005(45K)  → 290K

Key Takeaway

Stream Gatherers (JEP 485, Java 24) fill the gap between map/filter (stateless intermediates) and Collectors (terminal). They enable custom stateful intermediate operations: windowFixed for batching, windowSliding for pattern detection, and fold for running accumulations.

Before Gatherers, these patterns required ugly workarounds with IntStream.range or external libraries. Gatherers complement Collectors: Collectors customize HOW you produce the final result (terminal), Gatherers customize HOW you process elements IN the pipeline (intermediate).

At National Bank of India, batching transactions for bulk settlement uses gather(Gatherers.windowFixed(2)), and computing running balances uses gather(Gatherers.fold(...)), both in a clean pipeline.

Remember one sentence: Gatherers are to intermediate operations what Collectors are to terminal operations, enabling stateful windowing, batching, and accumulation mid-pipeline.

---

Share
Use ← → arrow keys to navigate