Understanding Gatherer and Groovy’s Collection Capabilities
The article explains how JDK 24’s Gatherer API extends Java streams with custom intermediate operations, compares its functionality to Groovy’s rich collection methods, and provides concrete Gatherer implementations for slicing, fixed and sliding windows, folding, and subsequence detection, complete with runnable code examples.
Java developers are familiar with Stream, but its built‑in intermediate operations (map, filter, limit, etc.) are limited for more complex scenarios. JDK 24 introduces Gatherer, a new intermediate operation that lets developers define custom stateful processing via four components: an initializer ( Supplier), an integrator, a finisher, and an optional combiner for parallel pipelines.
The core integrator interface is:
interface Integrator<A, T, R> {
boolean integrate(A state, T element, Downstream<? super R> downstream);
}Here state holds the mutable accumulator (often a list), element is the current stream element, and downstream pushes results downstream. JDK also ships built‑in gatherers such as windowFixed, windowSliding, and fold, which cover many common use‑cases.
Groovy collection slicing and window basics
Before diving into Gatherer, the article shows Groovy’s flexible slicing syntax using ranges and index lists, e.g.:
assert (1.8)[0..2] == [1, 2, 3]
assert (1.8)[3..<6] == [5, 6]
assert (1.8)[0,2,4,6] == [1, 3, 5, 7]
assert (1.8)[1,3,5,7] == [2, 4, 6, 8]These operations correspond to stream skip + limit patterns.
Fixed and sliding windows
Groovy’s collate creates fixed‑size blocks:
assert (1.8).collate(3) == [[1,2,3],[4,5,6],[7,8]]JDK’s Gatherers.windowFixed reproduces this behavior:
assert (1.8).stream().gather(windowFixed(3)).toList() == [[1,2,3],[4,5,6],[7,8]]When the last block should be dropped, a custom gatherer windowFixedTruncating is provided, which uses ofSequential and a greedy integrator to emit only full windows.
<T> Gatherer<T, ?, List<T>> windowFixedTruncating(int windowSize) {
Gatherer.ofSequential(
() -> [],
Gatherer.Integrator.ofGreedy { window, element, downstream ->
window << element
if (window.size() < windowSize) return true
var result = List.copyOf(window)
window.clear()
downstream.push(result)
}
)
}Key points: use ofSequential for ordered processing and ofGreedy because the algorithm never stops early; also validate windowSize < 1 in production.
Sliding windows with step size
Groovy’s collate overloads allow a step size, matching JDK’s Gatherers.windowSliding. A custom windowSlidingByStep gatherer handles arbitrary step sizes and optional retention of the final partial window:
<T> Gatherer<T, ?, List<T>> windowSlidingByStep(
int windowSize,
int stepSize,
boolean keepRemaining = true
) {
int skip = 0
Gatherer.ofSequential(
() -> [],
Gatherer.Integrator.ofGreedy { window, element, downstream ->
if (skip) { skip--; return true }
window << element
if (window.size() < windowSize) return true
var result = List.copyOf(window)
skip = stepSize > windowSize ? stepSize - windowSize : 0
[stepSize, windowSize].min().times { window.removeFirst() }
downstream.push(result)
},
(window, downstream) -> {
if (keepRemaining) {
while (window.size() > stepSize) {
downstream.push(List.copyOf(window))
stepSize.times { window.removeFirst() }
}
downstream.push(List.copyOf(window))
}
}
)
}Two important observations are noted: when stepSize > windowSize the skipped elements never enter a window, and the finisher is required only when keepRemaining is true.
Variable‑size chunking
Groovy’s chop allows each chunk to have a different size, with -1 meaning “consume the rest”. JDK lacks a built‑in gatherer for this, so a custom windowMultiple is shown, driven by a list of step sizes:
<T> Gatherer<T, ?, List<T>> windowMultiple(int... steps) {
var remaining = steps.toList()
Gatherer.ofSequential(
() -> [],
Gatherer.Integrator.of { window, element, downstream ->
if (!remaining) return false
window << element
if (remaining[0] != -1) remaining[0]--
if (remaining[0]) return true
remaining.removeFirst()
var result = List.copyOf(window)
window.clear()
downstream.push(result)
},
(window, downstream) -> { if (window) downstream.push(List.copyOf(window)) }
)
}Inject, reduce, scan and parallelPrefix
Groovy’s inject is analogous to reduce but more flexible because it can change the result type. Example converting a number sequence to a string:
assert (1.5).inject('') { s, n -> s + n } == '12345'The equivalent stream version uses fold as a gatherer:
assert (1.5).stream()
.gather(fold(() -> '', (s,n) -> s + n))
.findFirst().get() == '12345'For cumulative sums, Groovy’s inject can be written as:
assert (1.5).inject([]) { acc, next ->
acc + [acc ? acc.last() + next : next]
} == [1,3,6,10,15]Java provides Arrays.parallelPrefix for the same purpose on arrays.
Subsequence detection with inits/tails
Using Groovy’s inits and tails, the article builds a symmetric solution to test whether one list is a subsequence of another, then shows how to express the same logic with custom gatherers tails(), initsOfTails(), and a stream‑based inits() that pushes prefixes as they are built.
<T> Gatherer<T, ?, List<T>> tails() {
Gatherer.ofSequential(
() -> [],
Gatherer.Integrator.ofGreedy { state, element, downstream ->
state << element
return true
},
(state, downstream) -> { state.tails().each(downstream::push) }
)
}These implementations illustrate that while many algorithms are easier to write with list‑based APIs, Gatherer enables a more streaming‑friendly style when appropriate.
When to use Gatherer
The final section emphasizes that not every problem should be solved with streams, but when a stream fits, Gatherer fills the gap of expressive intermediate operations, allowing developers to write concise, problem‑centric pipelines instead of cumbersome custom collectors.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
