Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Java Stream Gatherers let you create custom intermediate operations: steps that sit between a stream’s source and its terminal operation. Use one when ordinary operations such as map and filter cannot express the needed state, buffering, variable output, or short-circuiting cleanly. Java SE 24 includes built-in gatherers for windows, folds, scans, and bounded concurrent mapping.

What are Java Stream Gatherers?

A Gatherer models a transformation from upstream stream elements to downstream elements. Unlike a simple map, it can retain state, emit no output for an input, emit several outputs, or wait until input ends before producing output. That makes it useful for operations with patterns or buffering that are awkward to express as a chain of standard intermediate operations.

A Collector gathers elements at the end of a pipeline as a terminal operation, commonly producing a collection or aggregate. A Gatherer is an intermediate operation: its output remains a stream that can flow into later pipeline steps. Oracle describes it as transforming stream input to stream output and optionally applying a final action at end of input: Java SE 24 Gatherer API.

The API is standardized in Java SE 24. Oracle marks the Gatherers utility class as available since Java 24; the dev.java Gatherers guide also identifies JDK 24 as the starting point.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When should you use gather() instead of map, filter, or reduce?

Prefer ordinary Stream operations when they state the job clearly: map transforms each element, filter keeps or rejects elements, and terminal reductions aggregate a stream. Choose gather() when the operation itself must be an intermediate pipeline stage and needs remembered context, variable output cardinality, thresholded buffering, pattern detection, or domain-specific early termination.

  • Use a built-in gatherer if its documented behavior matches the requirement.
  • Use a custom gatherer when a normal map/filter chain obscures state or output timing.
  • Decide whether parallel execution is valid before writing the implementation; state that cannot be merged safely should not pretend to support parallel combination.

How do you write a custom Gatherer in Java 24?

The simplest custom operation can be a stateless transformation, such as uppercasing each string. It is equivalent in effect to map(String::toUpperCase), so it is primarily useful for learning the API; for production code, use map unless a custom gatherer adds meaningful behavior.

import java.util.stream.Gatherer;

Gatherer<String, ?, String> uppercase = Gatherer.of(
    (element, downstream) -> downstream.push(element.toUpperCase())
);

var result = words.stream()
    .gather(uppercase)
    .toList();

Gatherer.of(...) provides a concise way to supply an integrator when the operation needs no explicit mutable state, combiner, or finisher. The downstream object is how the gatherer pushes output values. The Stream API’s gather method places that operation in the intermediate pipeline.

The four Gatherer functions

  • initializer() creates the mutable state for an operation. It is useful for buffers, counters, and other context accumulated while processing.
  • integrator() receives the current state, the next input, and a downstream sink. It may push zero or more outputs, then indicates whether additional upstream input should be accepted.
  • combiner() merges two partial states. It is the part that makes the gatherer’s own state combinable for parallel processing.
  • finisher() performs end-of-input work, such as emitting a buffered remainder that never encountered a later input to trigger a flush.

The integrator is the essential per-element function. The other functions are optional when the operation does not need their responsibilities. For detailed type and contract information, see Oracle’s Gatherer interface documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example: buffer consecutive error records

Suppose a log stream contains wrapper records with a severity and message. The desired operation groups consecutive ERROR records, but emits the group only when it reaches a configured threshold. A normal record ends the current run: the gatherer emits a qualifying run, discards a shorter run, clears its state, and continues. At end of input, the finisher must apply the same threshold to any trailing run.

// Illustrative structure; LogWrapper and severity accessors are domain-specific.
Gatherer<LogWrapper, List<LogWrapper>, LogWrapper> errorRuns(int threshold) {
    return Gatherer.ofSequential(
        ArrayList::new,
        (buffer, record, downstream) -> {
            if (record.isError()) {
                buffer.add(record);
                return true;
            }

            if (buffer.size() >= threshold) {
                for (LogWrapper item : buffer) {
                    if (!downstream.push(item)) {
                        return false;
                    }
                }
            }
            buffer.clear();
            return downstream.push(record);
        },
        (buffer, downstream) -> {
            if (buffer.size() >= threshold) {
                for (LogWrapper item : buffer) {
                    if (!downstream.push(item)) {
                        break;
                    }
                }
            }
        }
    );
}

This illustrates the key lifecycle rather than a universal log policy: the state is a mutable list, the integrator recognizes run boundaries and forwards output, and the finisher handles a run that reaches end-of-stream. The example is deliberately sequential because a run’s boundary and threshold depend on encounter order. If the downstream rejects further values, stop pushing promptly so short-circuiting can propagate rather than doing unnecessary work. The DZone custom-gatherer walkthrough covers this buffering pattern and its sequential constraint: Java Gatherers by Hüseyin Akdoğan.

How do Java 24’s built-in gatherers differ?

Oracle’s Java SE 24 Gatherers API supplies common stateful patterns. Their output shapes and memory behavior help determine whether a built-in fits better than a custom implementation.

Gatherer Output shape Typical use Ordering and parallel considerations Memory or caveat
windowFixed(n) Many inputs to lists of up to n elements Non-overlapping batches Groups elements in encounter order; do not assume parallel execution is useful for an order-sensitive consumer. The final window may be shorter. Returned lists are unmodifiable; large windows can allocate substantial memory eagerly.
windowSliding(n) Many inputs to overlapping lists of up to n elements Rolling windows, such as evaluating each consecutive run of measurements Windows follow encounter order; overlap can increase processing work. Retains window elements, and overlap increases the amount of repeated data in emitted windows. Returned lists are unmodifiable.
fold(...) Many inputs to normally one result An ordered aggregate that should remain an intermediate operation Order-dependent; useful where no suitable combiner exists. Produces the aggregate result rather than every intermediate state.
scan(...) One output per accumulated prefix Running totals or a stream of evolving state snapshots Prefix values depend on encounter order. Emits each intermediate state, so downstream receives more values than a final-only aggregate.
mapConcurrent(limit, mapper) One mapped output per input Mapping tasks that can run concurrently with bounded concurrency Uses virtual threads and preserves encounter order in its output. limit must be positive; mapper failures can propagate through the pipeline.

Fixed and sliding windows

Use windowFixed(n) when each element belongs to one consecutive batch, such as preparing groups for downstream processing. Use windowSliding(n) when each output represents a rolling view in which neighboring windows overlap. Both are appropriate only when a list of elements is the desired output unit. Account for unmodifiable result lists and the memory cost of the chosen window size.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fold and scan

fold accumulates in encounter order and normally emits a single result, making it a fit for an intermediate aggregate when there is no useful parallel combiner. scan instead exposes each prefix state, such as cumulative totals after each input. Choose based on whether later pipeline stages need just the final aggregate or the entire progression.

Bounded concurrent mapping

mapConcurrent(limit, mapper) is for independent mapping work that benefits from bounded concurrency. Its concurrency limit must be greater than zero. It uses virtual threads and retains encounter order in the results, so completion timing does not rearrange the output. If the mapper throws, treat that as a pipeline failure rather than assuming an individual result will be silently skipped.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Can Stream Gatherers run in parallel?

Yes, but parallelism depends on the operation’s semantics. A gatherer’s combiner is what merges partial states when the gatherer itself participates in parallel processing. Without a combiner, the gatherer can still occur in a parallel stream pipeline, but its operation should be treated as sequential or otherwise constrained; parallelizing the surrounding pipeline does not make unmergeable state safe.

For order-sensitive state such as consecutive log runs, splitting the source can divide a run across partial states, so a correct combiner must define how those boundary states join. If no correct merge rule exists, use a sequential gatherer factory or a combiner that rejects parallel use, and document the constraint. Oracle’s Gatherer documentation defines the role of the combiner; the DZone example rejects combination for its inherently sequential rule.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Practical design checklist

  • Describe the required input-to-output cardinality: one-to-one, one-to-many, many-to-one, or many-to-many.
  • Choose the simplest built-in whose semantics match before implementing custom state.
  • Define when buffered data is emitted, discarded, or flushed at end-of-input.
  • Decide whether encounter order is part of correctness, not merely presentation.
  • Specify whether partial states can be combined correctly before claiming parallel support.
  • In stateful integrators, respect downstream rejection and avoid expensive pushes after short-circuiting.
  • Estimate retained elements for windows and buffers, especially when window sizes are large.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.