Collectors Improvements in Java 9

Written on August 16, 2016

Collectors is an addition to Java 8 which let you specify data processing queries by aggregating the elements of a Stream into various containers such as Map, List and Set. For example, you can create a Map of the sum of expenses for each year as follows using the groupingBy and summingLong collector from the Collectors class. In the rest of this article we will assume static imports when referring to static method from the Collectors class.

Map<Integer, Long> yearToSum 
    = purchases.stream()
               .collect(groupingBy(Expense::getYear,  
                                   summingLong(Expense::getAmount));

So what’s new in Java 9? In Java 9, two new collectors are added to the Collectors utility class:

For the rest of article we will assume the following Expense and Tag class definitions:

public class Expense {
   private final long amount;
   private final int year;
   private final List<Tag> tags;

   public Expense(long amount, int year, List<Tag> tags) {
       this.amount = amount;
       this.year = year;
       this.tags = tags;
   }

   public long getAmount() {
       return amount;
   }

   public int getYear() {
       return year;
   }

   public List<Tag> getTags() {
       return tags;
   }
}

public class Tag {
   private final String content;

   public Tag(String content) {
       this.content = content;
   }
}

filtering

Let’s revisit the example above and say you now need to build a Map of the list of expenses for each year but only for expenses that are higher than GBP1000.

You already know how to generate a Map of the list of expenses for each year as follows:

Map<Integer, List<Expense>> yearToExpenses
    = purchases.stream()
               .collect(groupingBy(Expense::getYear));

So you could add a filter to the streams as follows:

Map<Integer, List<Expense>> yearToExpenses
    = purchases.stream()
               .filter(expense -> expense.getAmount() > 1_000)
               .collect(groupingBy(Expense::getYear));

Unfortunately, this means that if all expenses’ amount for a certain year are below £1000, the resulting Map would not contain an entry for that year (i.e. no key and no value).

Instead you can use the filtering collector as follows which would preserve the year in the resulting Map and produce an empty list:

Map<Integer, List<Expense>> yearToExpensiveExpenses
    = purchases.stream()
               .collect(groupingBy(Expense::getYear,
                                   filtering(expense -> expense.getAmount() > 1_000, toList())));

flatMapping

The flatMapping collector is the bigger brother of the mapping collector. Let’s say you need to produce a map of year with a set of tags from the expenses for each year. In other words, you need to produce Map<Integer, Set<Tag>>.

A first attempt may look as follows:

expenses.stream()
        .collect(groupingBy(Expense::getYear, 
                            mapping(Expense::getTags, toSet())));

Unfortunately this query returns a Map<Integer, Set<List<Tag>>>

By using flatMapping, you can flatten the intermediate Lists into a single container. The flatMapping collector takes two arguments:

  1. A function from one element to a Stream of elements
  2. A downstream collector to collect the single flattened stream into a container

With this knowledge you can solve the query as follows:

Map<Integer, Set<String>> 
    = expenses.stream()
              .collect(groupingBy(Expense::getYear, 
                                  flatMapping(expense -> expense.getTags().stream(), toSet())));

Note that the flatMapping collector is related to the flatMap method from the Stream API. The flatMap method takes a function producing a Stream of zero or more elements for each element in the input Stream. The result is then flattened in a single stream.

More collectors

If you are interested about more collectors, take a look at the Eclipse collections 8.0 which provides additional collectors such as zip, zipWithIndex and chunk. You can also always write your own custom collector by implementing the Collector interface.

If you are interested in an intensive Java course for your team, check out our Java Software Development Bootcamp or Modern Development with Java.