Long Time, No See, Stream Collector Utility

The stream-collector-utilities library has provided several useful collectors for Java 17 up to this point. Since it was starting to show its age, it was time for an overhaul. The library now ships four ready-to-use Gatherers that complement its existing Collector classes. This post walks through each of them.

What is a Gatherer?

Java 24 quietly introduced one of the most useful stream additions in years: java.util.stream.Gatherer. If you have ever reached for a Collector only to realize you actually needed an intermediate operation, something that transforms elements mid-pipeline rather than collapsing the whole stream into a result, then Gatherers are exactly what you were missing.

Gatherer<T, A, R> sits between map and filter on one side and collect on the other. Like a Collector it can hold state (A), but unlike a Collector it emits zero or more elements of type R per input element and can short-circuit the stream early. You plug it in with Stream.gather(…​).

Chunking without collecting

PortionsCollector groups elements into fixed-size chunks and returns them all as a Collection<List<T>>PortionsGatherer does the same chunking but emits each chunk immediately as the stream flows, so downstream stages can process chunks one by one:

Stream.of("a", "b", "c", "d", "e")
    .gather(PortionsGatherer.toLists(2))
    .map(chunk -> String.join("+", chunk))
    .forEach(System.out::println);
a+b
c+d
e

toSets variant is available for the same chunking into Set instances. Both variants are sequential.

Unwrapping Optionals inline

Unwrapping a stream of Optional<T> usually means a flatMap or a custom CollectorOptionalsGatherer makes this a one-liner and keeps it readable:

List<Optional<String>> list = List.of(
    Optional.of("alpha"), Optional.empty(), Optional.of("beta"), Optional.empty());

// drop empties
list.stream()
    .gather(OptionalsGatherer.withoutEmpty())
    .forEach(System.out::println);  // alpha, beta

// keep empties as null
list.stream()
    .gather(OptionalsGatherer.withEmpty())
    .forEach(System.out::println);  // alpha, null, beta, null

Both variants are parallel-capable because they carry no shared state.

Truncate with style, and stop early

EllipsisCollector appends elements until a character budget is exhausted, then joins everything into a single string with a trailing ellipsis. EllipsisGatherer does the same, but as an intermediate operation and crucially it short-circuits the stream the moment the budget is exceeded:

List<String> brothers = List.of("Chico", "Harpo", "Groucho", "Gummo", "Zeppo");

List<CharSequence> result = brothers.stream()
    .gather(EllipsisGatherer.ellipsis(16))
    .toList();

System.out.println(String.join(", ", result));
Chico, Harpo, …

Because the stream short-circuits, elements after the budget is hit are never even consumed. For large or lazily-generated streams this can be a meaningful performance win over the Collector equivalent. A custom delimiter and ellipsis marker are supported via the three-argument overload.

Range tokens on the fly

RangesCollector turns a sorted sequence of integers into a compact range string such as 101-103,105-108,110RangesGatherer emits each individual range token as it becomes complete:

List<Integer> numbers = List.of(101, 102, 103, 105, 106, 107, 108, 110);

numbers.stream()
    .gather(RangesGatherer.ranges())
    .forEach(System.out::println);
101-103
105-108
110

This makes it easy to filter, count, or map individual range tokens before joining. All three Compactness modes (RANGEOFFSETRANGE_OFFSET) are supported, matching the Collector API exactly. To reproduce the Collector output, pipe into Collectors.joining(",").

Requirements

All Gatherers require Java 25 or later. The java.util.stream.Gatherer API was finalized in Java 24, and the library itself is compiled with --release 25.

The library is available on Maven Central:

<dependency>
    <groupId>de.schegge</groupId>
    <artifactId>stream-collector-utilities</artifactId>
    <version>1.0.0</version>
</dependency>

Source and full documentation: https://gitlab.com/schegge/stream-collector-utilities

Leave a Comment