JDK 25's Stream Gatherer Example

Stream Gatherers are basically custom superpowers for your Stream API (they return another Stream and mostly just chill until a terminal operation shows up to actually do the work, basically intermediate operations). This JEP is like giving the Stream API steroids: now you can bend streams to your will in ways that would make previous Java developers weep tears of pure joy.
This article is a part of the original article here.
Stream Gatherers (JEP 485)
Current status: Finalized in JDK 24.
The Example
Take this classic headache: filtering by a field that's not the ID.
You've got two choices: wrap your objects in some custom contraption or drag Google Guava into the party with its Equivalence wizardry:
The Data Class
public record Person(Integer id, String name) {}
The Implementations
- Use Stream Gatherer
- Use Guava's Equivalence
First we create our own gatherer:
public class DistinctByNameGatherer
implements Gatherer<Person, Set<String>, Person> {
@Override
public Supplier<Set<String>> initializer() {
// Create the state: a HashSet that will track which names we've seen
return HashSet::new;
}
@Override
public Integrator<Set<String>, Person, Person> integrator() {
return Integrator.ofGreedy(
(state, element, downstream) -> {
// Extract the name from the Person element
var extracted = element.name();
// If we haven't seen this name before...
if (!state.contains(extracted)) {
// Remember it for posterity
state.add(extracted);
// Push the Person downstream (keep it in the stream)
downstream.push(element);
}
// Always return true because we're optimists
return true;
});
}
}
Then we apply our beautiful creation:
persons.stream()
.gather(new DistinctByNameGatherer())
.toList();
import com.google.common.base.Equivalence;
persons.stream()
.map(Equivalence.equals().onResultOf(Person::name)::wrap)
.distinct()
.map(Equivalence.Wrapper::get)
.toList();
We create an equivalence for Person objects using Google Guava's Equivalence class (because apparently we need an entire library to compare names). We wrap each object, apply distinct() to yeet the duplicates, then unwrap everything. Three stream operations to do what should be simple: peak Java energy!
You can dig into JEP 485 for the nerdy details, or just download JDK 24 and explore the java.util.stream.Gatherers class for some sweet built-in examples!
Deep-dive by José Paumard
You can see the videos by José Paumard for a more comprehensive explanation: