Posts Tagged ‘StringTemplates’
[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.
Links:
[DevoxxBE2023] Java Language Update by Brian Goetz
At Devoxx Belgium 2023, Brian Goetz, Oracle’s Java Language Architect, delivered an insightful session on the evolution of Java, weaving together a narrative of recent advancements, current features in preview, and a vision for the language’s future. With his deep expertise, Brian illuminated how Java balances innovation with compatibility, ensuring it remains a cornerstone of modern software development. His talk explored the introduction of records, sealed classes, pattern matching, and emerging features like string templates and simplified program structures, all designed to enhance Java’s expressiveness and accessibility. Through a blend of technical depth and practical examples, Brian showcased Java’s commitment to readable, maintainable code while addressing contemporary programming challenges.
Reflecting on Java’s Recent Evolution
Brian began by recapping Java’s significant strides since his last Devoxx appearance, highlighting features like records, sealed classes, and pattern matching. Records, introduced as nominal tuples, provide a concise way to model data with named components, enhancing readability over structural tuples. For instance, a Point record with x and y coordinates is more intuitive than an anonymous tuple of integers. By deriving constructors, accessors, and equality methods from a state declaration, records eliminate boilerplate while making a clear semantic statement about data immutability. Brian emphasized that this semantic focus, rather than mere syntax reduction, distinguishes Java’s approach from alternatives like Lombok.
Sealed classes, another recent addition, allow developers to restrict class hierarchies, specifying permitted subtypes explicitly. This enables libraries to expose abstract types while controlling implementations, as seen in the JDK’s use of method handles. Sealed classes also enhance exhaustiveness checking in switch statements, reducing runtime errors by ensuring all cases are covered. Brian illustrated this with a Shape hierarchy, where a sealed interface permits only Circle and Rectangle, allowing the compiler to verify switch completeness without a default clause.
Advancing Data Modeling with Pattern Matching
Pattern matching, a cornerstone of Java’s recent enhancements, fuses type testing, casting, and binding into a single operation, reducing errors from manual casts. Brian demonstrated how type patterns, like if (obj instanceof String s), streamline code by eliminating redundant casts. Record patterns extend this by deconstructing objects into components, enabling recursive matching for nested structures. For example, a Circle record with a Point center can be matched to extract x and y coordinates in one expression, enhancing both concision and safety.
The revamped switch construct, now an expression supporting patterns and guards, further leverages these capabilities. Brian highlighted its exhaustiveness checking, which uses sealing information to ensure all cases are handled, as in a Color interface sealed to Red, Yellow, and Green. This eliminates the need for default clauses, catching errors at compile time if the hierarchy evolves. By combining records, sealed classes, and pattern matching, Java now supports algebraic data types, offering a powerful framework for modeling complex domains like expressions, where a sealed Expression type can be traversed elegantly with pattern-based recursion.
Introducing String Templates for Safe Aggregation
Looking to the future, Brian introduced string templates, a preview feature addressing the perils of string interpolation. Unlike traditional concatenation or formatting methods, string templates use a template processor to safely combine text fragments and expressions. A syntax like STR.FMT."Hello, \{name\}!" invokes a processor to validate inputs, preventing issues like SQL injection. Brian envisioned a SQL template processor that balances quotes and produces a result set directly, bypassing string intermediaries for efficiency and security. Similarly, a JSON processor could streamline API development by constructing objects from raw fragments, enhancing performance.
This approach reframes interpolation as a broader aggregation problem, allowing developers to define custom processors for domain-specific needs. Brian’s emphasis on safety and flexibility underscores Java’s commitment to robust APIs, drawing inspiration from JavaScript’s tagged functions and Scala’s string interpolators, but tailored to Java’s ecosystem.
Simplifying Java’s On-Ramp and Beyond
To make Java for new developers, Brian discussed preview features like unnamed classes and patterns, which reduce boilerplate for simple programs. A minimal program might omit public static void main, allowing beginners to focus on core logic rather than complex object-oriented constructs. This aligns Java with languages like Python, where incremental learning is prioritized, easing the educational burden on instructors and students alike.
Future enhancements include reconstruction patterns for immutable objects, enabling concise updates like p.with(x: 0) to derive new records from existing ones. Brian also proposed deconstructor patterns for regular classes, mirroring constructors to enable pattern decomposition, enhancing API symmetry. These features aim to make aggregation and decomposition reversible, reducing error-prone asymmetries in object manipulation. For instance, a Person class could declare a deconstructor to extract first and last names, mirroring its constructor, streamlining data handling across Java’s object model.
Conclusion: Java’s Balanced Path Forward
Brian’s session underscored Java’s deliberate evolution, balancing innovation with compatibility. By prioritizing readable, maintainable code, Java addresses modern challenges like loosely coupled services and untyped data, positioning itself as a versatile language for data modeling. Features like string templates and simplified program structures promise greater accessibility, while pattern matching and deconstruction patterns enhance expressiveness. As Java continues to refine its features, it remains a testament to thoughtful design, ensuring developers can build robust, future-ready applications.