Java 8: Accumulate the elements of a Stream using Collectors

I’ve earlier written about the Stream API and how you can write more declarative code by using it.

In this article I want to focus on the different ways of accumulating the elements of a Stream using Collectors.

This is made possible by using collect — a method in the Stream interface.

collect takes a Collector, which is an interface for reduction operations.

Today, I'll take a look at Collectors, which contains several implementations of the Collector interface.

To get familiar with these Collector implementations, let’s play around with a stream of articles — Stream<Article>.

Converting a Stream to a List

Let’s start with the absolute basic — converting a Stream to a List.

articles.collect(Collectors.toList());  

That’s it — you just use Collectors.toList which returns a collector that is sent to the collect method.

Converting a Stream to a Map

Converting a Stream to a Map is almost as easy as converting to a List.

To see how it’s done, let’s convert the stream of articles to a map where the title is the key and the article is the value.

articles.collect(Collectors.toMap(Article::getTitle, Function.identity()));  

This time we use Collectors.toMap, which takes a function for creating the keys and a function for creating the values.

In our example we use Article::getTitle to say that we want the titles as keys.

For the value mapping, we use Function.identity. This simply returns a function that always returns its input parameter — in our case the Article — as output.

Calculating averages

Next, let’s look at how we can calculate averages.

Let’s say that each article contains the number of words in the article text, and we want to check how many words our articles contain in average.

articles.collect(Collectors.averagingInt(Article::getWordCount));  

To do this we use the averageInt which takes a ToIntFunction where we can map to the count of words.

This arithmetic collector also exists for double and long as well.

Summing

While we’re at it, let’s look at another arithmetic collector.

This time let’s look at how we can calculate the total number of words in all our articles combined.

articles.collect(Collectors.summingInt(Article::getWordCount));  

Joining strings

Now that we’ve looked at how we can do arithmetic operations on our streams, let’s move on by looking at how we can join strings.

Say, we would like to collect all the titles in one string.

To do this, let’s use joining.

Joining comes in a few different versions.

articles.map(Article::getTitle)  
        .collect(Collectors.joining());

// Resulting string: title1title2

This version simply joins the strings together.

However, this isn’t very readable, so let’s look at another variant of the joining method where we can set a delimiter.

articles.map(Article::getTitle)  
        .collect(Collectors.joining(", "));

// Resulting string: title1, title2

That’s better, but there is actually one more version where you also can add a prefix and suffix to the resulting string.

articles.map(Article::getTitle)  
        .collect(Collectors.joining(", ", "Articles: ", "."));

// Resulting string: Articles: title1, title2.

Get an element that is maximum or minimum according to a Comparator

You also can find an element that is maximum or minimum according to a Comparator.

Let’s say you want to get the article containing the most words.

Comparator<Article> wordCountComparator = (a1, a2) -> a1.getWordCount() - a2.getWordCount();

articles.collect(Collectors.maxBy(wordCountComparator));  

Now if we want to find the article containing the least words, we could use minBy.

Comparator<Article> wordCountComparator = (a1, a2) -> a1.getWordCount() - a2.getWordCount();

articles.collect(Collectors.minBy(wordCountComparator));  

Partition your stream

Now let’s look at a really cool collector that is partitioning your stream based on a predicate.

Let’s say we would like to split the articles in two based on the count of words being more or less than 1000 words.

articles  
  .collect(Collectors.partitioningBy(article -> article.getWordCount() > 1000));

This will result in a map with two entries. One with the key true which contains the articles that satisfied the predicate and one with key false with the articles that didn’t satisfy the predicate.

Do additional work on the result of a collector

You may want to do some more work on the result of a collector.

Let’s say you want to add the average word count to a string.

Then you can use collectingAndThen to append a function that will be executed on the result of the collector.

articles.collect(  
  Collectors.collectingAndThen(
    Collectors.averagingInt(Article::getWordCount),
    average -> "Average count: " + average));

Create a map by grouping

groupBy is a simple yet powerful way to collect your stream into a map.

Let’s look at two variants of groupBy.

The first one simply requires to say what you want to group your elements by.

To see how this works, let’s group our data based on an articles tag.

articles.collect(Collectors.groupingBy(Article::getTag));  

This will return a map with tags mapping to a list of the associated articles.

Let’s now say we want to have the tags mapping to the total number of words of all the articles for a given tag.

articles.collect(  
  Collectors.groupingBy(
    Article::getTag,
    Collectors.summingInt(Article::getWordCount)));

In this version we add an extra collector where we say we want to reduce the associated articles into the total count of words.

Map then collect

If you’re familiar with streams, you know about the map method.

So in a scenario where you would like to create a list of the titles of your articles, you could solve this by first mapping to the titles before collecting them using toList.

articles  
  .map(Article::getTitle)
  .collect(Collectors.toList());

Another option is to use Collectors.mapping.

articles.collect(Collectors.mapping(Article::getTitle, Collectors.toList()));  

This may seem like an unnecessary collector, since you got the map function, but it actually could be quite helpful.

Consider the following.

We want to group our stream of articles by the tag, but we only want to keep the titles in the values.

To solve this, we need to keep the whole Article object long enough to do the grouping, but map to titles afterwards.

This is where mapping comes handy.

articles.collect(  
  Collectors.groupingBy(
    Article::getTag, 
    Collectors.mapping(Article::getTitle, Collectors.toList())));

By using mapping we’re able to first do grouping based on the whole Article object, before mapping it to titles.

Further reading

Make sure to checkout the documentation for a complete list of available collectors.

Enjoyed the post?

If you don't want to miss future posts, make sure to subscribe