Posts Tagged ‘StreamGatherers’
[VoxxedDaysAmsterdam2026] Stream Tricks That You Don’t Wanna Miss: Enhancing Java Streams with Gatherers and String Templates in JDK 25
Lecturer
Aicha Laafia is a Java software engineer at Havana Group, currently based in France while originally from Morocco. She is passionate about sustainable technology, green programming, and advocating for greater representation of women in tech. Aicha actively participates in various communities, serves as a Women Techmakers and Girls Code ambassador, and facilitates IAmRemarkable workshops. She was recently promoted to Oracle ACE Associate, recognizing her contributions to the Java ecosystem.
Abstract
In this engaging session from Voxxed Days Amsterdam 2026, Aicha Laafia explores significant enhancements to Java’s stream processing capabilities and string handling introduced in JDK 25. She addresses longstanding pain points with traditional streams—such as the inability to maintain state mid-pipeline, complex custom collectors for batching or sliding windows, and error-prone string concatenation for SQL, JSON, or logs—through the new Stream Gatherers API and String Templates. Drawing on live code demonstrations and relatable examples from Formula 1 racing data, the presentation illustrates how these features introduce memory and statefulness to streams, simplify data transformations, and promote safer, more readable code. The talk underscores Java’s continued evolution toward more expressive and maintainable programming paradigms, encouraging developers to upgrade and share knowledge about these advancements.
The Persistent Challenges with Traditional Java Streams
Java developers have long appreciated streams for producing cleaner, more declarative, and expressive code compared to imperative loops. However, as Aicha points out, streams can occasionally leave programmers feeling frustrated or even “like complete idiots” when attempting advanced operations. The core limitation stems from the stateless nature of intermediate operations like map, filter, or flatMap. Each element processes independently and is immediately forgotten, making it impossible to track accumulated state, create overlapping windows, or group data mid-pipeline without terminating the stream via a collector.
Common pain points include manual batching implementations that rely on counters, lists, and careful index management to avoid off-by-one errors or lost elements. Grouping overlapping data—for instance, creating sliding windows of size n for rolling averages or trend detection—often requires intricate custom collectors that become difficult to understand or maintain over time, even for the original author. Furthermore, once a collector is applied, the pipeline ends; no further stream operations are possible afterward. These issues lead to verbose, error-prone code or a reluctant fallback to traditional for-loops, undermining the very benefits streams were meant to deliver.
Aicha emphasizes that these problems arise because prior to JDK 25, streams lacked “memory.” Elements flowed through independently without retaining context from previous items, forcing developers into workarounds that compromised readability and maintainability.
Introducing Stream Gatherers: Bringing Memory and Flexibility to Streams
JDK 25 addresses these limitations head-on with the Stream Gatherers API, which equips streams with stateful processing capabilities while remaining intermediate operations. Unlike terminal collectors, gatherers allow continued chaining after stateful transformations. A gatherer consists of up to four components, though only the integrator is mandatory:
- Initializer (optional): Executes once before any elements arrive, establishing initial state such as an empty list or counter.
- Integrator: The core logic, invoked for every element. It receives the current element, the mutable state, and a downstream consumer. Developers implement accumulation or transformation here, returning
trueto continue orfalseto short-circuit the pipeline. - Combiner (optional): Essential for parallel streams, merging partial states from different threads.
- Finisher (optional): Runs once at the end of the stream, ensuring no residual state (such as an incomplete final batch) is lost by pushing any remaining elements downstream.
This design provides a short-circuit mechanism and supports parallel execution when a combiner is supplied. Aicha demonstrates creating a custom batching gatherer in roughly 15 lines of code—far simpler than equivalent custom collectors or manual loops. The initializer creates an empty list; the integrator adds elements until the batch size is reached, then pushes the batch downstream and clears the buffer; the finisher handles any trailing incomplete batch.
Even better, JDK 25 ships with five built-in gatherers that eliminate most custom implementations:
windowFixed(n): Produces non-overlapping batches of exactly size n, including a final potentially smaller batch.windowSliding(n): Generates overlapping windows, ideal for rolling calculations, trend detection, or analyzing sequential data patterns in production monitoring.scan: Accumulates intermediate results similar to a fold, emitting every partial value starting from an initial element—unlikereduce, which yields only the final result.fold: Similar accumulation but treats the operation as intermediate, returning anOptionalwhile permitting further pipeline chaining.mapConcurrent(maxConcurrency, mapper): Executes the mapper on virtual threads (up to the specified concurrency limit) while preserving encounter order, making it particularly suited for I/O-bound tasks without manual thread management.
These tools transform previously cumbersome tasks into concise, readable one- or few-line operations.
Live Demonstration: Analyzing Formula 1 Data with Gatherers
To illustrate practical application, Aicha uses racing data from Max Verstappen’s 2025 Formula 1 season, modeled as a record containing round number, Grand Prix name, position, and points. She contrasts traditional approaches—often involving dozens of lines of custom collector code with initializer, accumulator, combiner, and finisher—with gatherer-based solutions.
For batching every three races to compute cumulative points and wins, a windowFixed(3) gatherer replaces extensive custom logic, producing clean batches while automatically handling the final incomplete group. Sliding windows demonstrate overlapping views, such as performance trends across consecutive race triplets, again in just a few lines.
Accumulation across the entire season uses scan to emit running totals after each race, revealing Verstappen’s final 421 points and near-miss championship outcome. These examples highlight how gatherers retain “memory” of prior elements, enabling stateful yet fluent pipelines.
Aicha also touches on String Templates, another JDK 25 feature that enhances safety and readability. Traditional string concatenation or String.format often leads to injection vulnerabilities in SQL or JSON and creates “plus soup” that is hard to read. String Templates provide a clean, type-safe interpolation mechanism that reduces errors and improves security for logging, queries, and data serialization.
Implications and Recommendations for Modern Java Development
The introduction of gatherers and string templates reflects Java’s ongoing commitment to evolving without breaking compatibility, offering developers more powerful abstractions while preserving the language’s robustness. By reducing reliance on custom collectors and imperative workarounds, these features promote more maintainable, expressive codebases that are easier to reason about and debug.
Gatherers particularly shine in data processing pipelines, analytics, monitoring, and any domain requiring windowed or accumulated views. Their support for parallelism and short-circuiting adds efficiency, while the built-in variants cover the majority of common use cases, lowering the barrier to advanced stream usage.
Aicha encourages the community to upgrade to the latest JDK, experiment with these capabilities, write articles, and deliver talks to spread awareness. She notes that many scenarios previously abandoned to “for-loop hell” now become elegant stream solutions thanks to gatherers.
Code Sample: Batching with windowFixed
// Traditional complex collector approach omitted for brevity
// With Gatherers in JDK 25
var batches = races.stream()
.gather(Gatherers.windowFixed(3))
.map(batch -> computeStats(batch)) // e.g., sum points, count wins
.toList();
Code Sample: Sliding Window for Trends
var slidingWindows = races.stream()
.gather(Gatherers.windowSliding(3))
.map(window -> analyzeTrend(window))
.toList();
Code Sample: Accumulation with scan
var runningTotals = pointsStream
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList(); // Emits every intermediate sum
These snippets demonstrate the dramatic reduction in complexity while preserving full pipeline fluency.
In conclusion, Aicha Laafia’s presentation provides both a clear diagnosis of historical stream limitations and a compelling vision for their resolution in JDK 25. By incorporating statefulness through gatherers and safer string handling, Java strengthens its position as a modern, versatile language suitable for complex data-driven applications. Developers who adopt these features will benefit from shorter, more readable code, fewer maintenance headaches, and enhanced productivity.