46

Java Stream Collectors Explained - Better Programming - Medium

 4 years ago
source link: https://medium.com/better-programming/java-stream-collectors-explained-6209a67a4c29
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Responses

You have 2 free member-only stories left this month.

Java Stream Collectors Explained

How do Collectors work — and how can we build our own?

Image for post
Image for post
Photo by Sharon McCutcheon on Unsplash

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. — Oracle

Batteries Included

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.
  • Grouping
    Three different ways to use #groupingBy(...) and another three for concurrent/parallel processing.
  • Partitioning
    Two #partitionBy(...) methods available.

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.

Generic types

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.

Methods

The methods make more sense knowing what the generic types represent:

  • supplier()
    Provides a Supplier<A> used for creating new instances of accumulator objects.
  • accumulator()
    The core of the Collector, including a BiConsumer<A, T> responsible for accumulating stream elements of type T into an accummulator object.
  • combiner()
    In the case of parallel processing a Stream, the Collector might create multiple accumulator objects. The combiner provides the functionality to merge the results.
  • finisher()
    Finishes the collection process by transforming an accumulator object into the return type R.
  • characteristics()
    Describes the characteristics of the Collector.

Collector characteristics

The characteristics of a Collector can be used to optimize the implementation of the reduction operation. Any combination of these three characteristics is possible:

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.

The interface 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.

Conclusion

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK