With Java 8 came one of the greatest additions to Java: the Stream API. It made processing a stream of data very convenient by allowing us to chain operations together, lazily, and perform the actual data processing by ending a fluent call with a terminal operation.
java.util.Stream provides two different terminal operations named
collect(...), which will perform a mutable reduction:
A mutable reduction operation accumulates input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream.
Java 8 provides us with a range of 37 different Collectors in the class
java.util.stream.Collectors, which can roughly be divided into three separate groups:
Reducing/summarizing into a single value or collection type
Everything from joining Strings with
joining()to creating new Collections with
toSet()to even leveraging new features like summaries of numeric streams with
summarizingInt(...)— and much more.
Three different ways to use
groupingBy(...)and another three for concurrent/parallel processing.
And the best thing: We’re not restricted to the provided Collectors. If we need some more unique handling, we can always create our own.
Collector<T, A, R>
If you ever checked out some of the source code of the Stream API, you’ll find a lot of generics and a lot of not easily readable or comprehensible code.
This originates from Java itself because it wasn’t easy to implement functional programming features without changing its core or changing the language itself. But they managed to add these great new features without compromising backward compatibility by some intimidating-looking code — at least at first glance.
Every Collector must implement the interface
Collector<T, A, R>:
Let’s dissect the interface a little bit to understand better what’s going on.
The interface consists of three generic types:
- T – the type of input elements to the reduction operation
- A – the mutable accumulation type of the reduction operation.
The accumulator object type for keeping partial results during the collection process.
- R – the result type of the reduction operation. The actual return type of the collection process.
The methods make more sense knowing what the generic types represent:
Supplier<A>used for creating new instances of accumulator objects.
The core of the
Collector, including a
BiConsumer<A, T>responsible for accumulating stream elements of type
Tinto an accummulator object.
In the case of parallel processing a Stream, the
Collectormight create multiple accumulator objects. The combiner provides the functionality to merge the results.
Finishes the collection process by transforming an accumulator object into the return type
Describes the characteristics of the
The characteristics of a
Collector can be used to optimize the implementation of the reduction operation. Any combination of these three characteristics is possible:
Indicates the accumulator objects support parallel or concurrent processing.
Indicates the finisher function is the identity function so the accumulator might be cast directly in the result type.
Indicates the order of elements in the stream isn’t necessarily preserved.
Example: Joining Strings
Java already provides a
Collector for joining Strings with a delimiter, but it makes for a good example to implement ourselves:
Simple enough — but it’s still a lot of code for very little functionality.
Collector provides the static method
of(...) to create a
Collector in a more functional way, helping us to reduce the need for an extra class:
Now we can combine our custom
Collector creator methods in an interface or noninstantiable class — as Java did it with
java.util.Collectors for more straightforward usage.
What About reduce(…)?
Instead of a
Collector, we could also use
Stream#reduce(...) to achieve similar results. The difference between the two is more subtle. A reduce operation creates a new value by combining two values in an immutable way.
A collect operation, however, is working with accumulate objects in a mutable way and uses a finisher to obtain the final result.
Which one you should prefer depends on your requirements — considering the actual intended purpose, performance considerations, etc.
Creating a custom
Collector isn’t complicated once you understand the general concepts behind them.
By combining our custom
Collector creator methods the way Java did, we can use and share our Collectors throughout our projects.