Core Java

Explore the gather Method in Java 22 java.util.stream

Java 22 unveils an exciting preview feature through JEP 461: Stream Gatherers, marking a significant enhancement to the Stream API. Let’s explore the gather method in Java 22 java.util.stream.

1. Introduction

Java’s Stream API, introduced in JDK 8, revolutionized data processing by providing a functional approach to handle sequences of elements. However, certain complex data transformations were not straightforward with the existing set of operations. To address these limitations, Java 22 introduced a preview feature known as Stream Gatherers through JEP 461. This feature introduces the gather method, enhancing the Stream API’s flexibility and expressiveness.

1.1 Understanding the Gather Method

The gather method is an instance method added to the Stream interface in Java 22. It allows developers to create custom intermediate operations by utilizing the Gatherer interface. A Gatherer defines how elements are accumulated and transformed within a stream pipeline. This approach provides a more versatile alternative to existing operations like map and flatMap.

A Gatherer in Java Stream API is a functional interface that allows developers to define custom accumulation and transformation strategies for streams. It is characterized by four main components, each playing a vital role in the accumulation process:

  • Initializer: This component provides the initial state or an empty accumulator that will hold the results as the stream is processed. It essentially sets up the base structure before any elements are integrated.
  • Integrator: The integrator defines how each element from the stream is integrated into the accumulator. This is where the logic for adding elements or transforming data as it’s processed is implemented.
  • Combiner: The combiner is responsible for merging two accumulators when processing the stream in parallel. This is crucial for handling parallel streams effectively, as it ensures that the partial results from different threads are combined into one cohesive result.
  • Finisher: The finisher is an optional component that processes the final state of the accumulator after all elements have been processed. It can transform the accumulated data into a final form, such as converting a collection into a different type, like a list or set.

These components work together to offer fine-grained control over stream transformations, allowing for complex data manipulations tailored to specific requirements.

1.2 Benefits of using Stream Gatherers

The introduction of Stream Gatherers offers several advantages:

  • Custom Intermediate Operations: Developers can define operations tailored to specific data processing needs, extending the Stream API’s capabilities.
  • Enhanced Code Reusability: Custom gatherers can be reused across different parts of an application, promoting cleaner and more maintainable code.
  • Improved Performance: By reducing the need for external state management and minimizing synchronization overhead, gatherers can lead to more efficient stream processing.

2. Simple Code Example

To utilize the gather method, you first need to define a Gatherer. Below is an example that demonstrates how to create a custom gatherer to collect elements into fixed-sized windows:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Gatherers.*;
 
public class StreamGatherExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
 
        List<List<Integer>> windows = numbers.stream()
            .gather(windowFixed(3))
            .collect(Collectors.toList());
 
        System.out.println(windows);
    }
}

2.1 Code Explanation and Output

The provided Java code demonstrates how to use the gather method in Java’s Stream API to group elements from a list into fixed-size windows. First, the code initializes a list of integers from 1 to 9 using Arrays.asList(). Then, it uses the stream() method to convert the list into a stream, followed by the gather(windowFixed(3)) operation, which groups the stream elements into windows of 3 integers each. Finally, the collect(Collectors.toList()) method collects the grouped elements into a list of lists. Keep in mind that you can modify the fixed number or make it dynamic according to your needs.

The output of the code is:

1
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

3. Custom Gatherer Example

Here’s a simple example that demonstrates how these components might work together in a custom Gatherer:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
 
public class CustomGathererExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
 
        // Custom Gatherer for overlapping sliding windows of 3 elements
        Gatherer<Integer, Deque<List<Integer>>> gatherer = new Gatherer<>() {
             
            // Initializer: Supplies a new deque to store overlapping windows
            @Override
            public Supplier<Deque<List<Integer>>> supplier() {
                return ArrayDeque::new// Creates an empty deque
            }
 
            // Integrator: Maintains a sliding window of the last 3 elements
            @Override
            public BiConsumer<Deque<List<Integer>>, Integer> accumulator() {
                return (deque, item) -> {
                    if (deque.size() > 0) {
                        List<Integer> lastWindow = new ArrayList<>(deque.peekLast()); // Copy last window
                        if (lastWindow.size() == 3) {
                            lastWindow.remove(0); // Remove the oldest element
                        }
                        lastWindow.add(item); // Add the new element
                        deque.addLast(lastWindow); // Store the new window
                    } else {
                        deque.addLast(Collections.singletonList(item)); // Initialize with first element
                    }
                };
            }
 
            // Combiner: Merges two deques while preserving windowing order
            @Override
            public BinaryOperator<Deque<List<Integer>>> combiner() {
                return (deque1, deque2) -> {
                    if (!deque1.isEmpty() && !deque2.isEmpty()) {
                        List<Integer> lastDeque1 = deque1.peekLast();
                        List<Integer> firstDeque2 = deque2.peekFirst();
                        if (lastDeque1.size() < 3 && firstDeque2.size() < 3) {
                            lastDeque1.addAll(firstDeque2); // Merge incomplete windows
                        }
                    }
                    deque1.addAll(deque2); // Merge the entire deques
                    return deque1;
                };
            }
 
            // Finisher: Converts the deque to a final list of sliding windows
            @Override
            public Function<Deque<List<Integer>>, List<List<Integer>>> finisher() {
                return ArrayList::new// Convert deque to a list
            }
        };
 
        // Process numbers with custom gatherer
        List<List<Integer>> slidingWindows = numbers.stream()
            .gather(gatherer)
            .collect(Collectors.toList());
 
        System.out.println(slidingWindows);
    }
}

3.1 Code Explanation

This Java program demonstrates a custom gatherer that implements a sliding window mechanism to process a list of integers in overlapping groups of three. It utilizes a Deque to store windows efficiently, ensuring that each new element extends the previous window while discarding the oldest one. The supplier() initializes an empty deque, and the accumulator() manages the sliding window logic by adding elements while maintaining a maximum size of three. The combiner() merges partial windows when executed in parallel streams, ensuring proper sequence merging. Finally, the finisher() converts the deque into a list.

3.2 Code Output

When executed with input [1, 2, 3, 4, 5, 6, 7, 8, 9], the program outputs the below showing how each window slides forward by one element.

1
[[1], [1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [6, 7, 8], [7, 8, 9]]

4. Conclusion

In conclusion, the introduction of the gather method in Java 22’s Stream API provides developers with enhanced flexibility and control over data processing. By enabling custom intermediate operations through the use of Stream Gatherers, Java allows for more expressive and efficient handling of data streams, especially for complex use cases like grouping elements or performing custom transformations. The ability to define gatherers with specific rules for accumulation and integration not only improves code clarity but also opens up new possibilities for optimizing performance and code reusability. As Stream Gatherers are currently a preview feature, developers need to enable them during compilation and runtime, making this a forward-looking feature that promises to be a valuable addition to the Java developer’s toolkit.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button