Java - Rewriting Traditional Codes With Streams #3 (all collectors with examples)

Ömer Kurular
9 min readFeb 13, 2022

--

We are continuing with the new part of the series. In this part, we will explore collecting elements that is produced after series of stream operations.

Collecting resultant elements: collect

When are working over collections, we may want to create a new collections from that collection after doing some filtering, mapping or other kind of operations. Below, we can see a very simple example of it.

private static List<Integer> getEvenNumbersFromList(List<Integer> list) {
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : list) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
return evenNumbers;
}

In this example, the function will get even numbers from the given list and collect them in another list.

In stream context, after doing series of stream events, you may just want to collect the resultant elements into a collection instead of iterating over using foreach or terminating it with terminal operations like anyMatch, noneMatch, sum etc. We are going to use Collectors class to achieve different kind of collection tasks. Let’s begin with the collect method of stream api.

Stream class has two collect method. One takes a Collector instance as only parameter and we are going the be using it mostly as Collectors class presents a lot of useful collection methods, and other one takes three parameters which are supplier, accumulator and combiner to collect stream elements.

<R, A> R collect(Collector<? super T, A, R> collector);<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);

Let’s see what Collectors class provides to achieve different kinds of collections.

1- Collectors.toList()

It returns a collector that collects the elements in a stream into a list. See how we can use this.

private static List<Integer> collectEvenNumbersFromList(List<Integer> list) {
return list.stream().filter(number -> number % 2 == 0).collect(Collectors.toList());
}

We are filtering the given integer list and then we are collecting elements to a list using Collectors.toList() method.

2- Collectors.toSet()

It returns a collector that collects the elements in a stream into a set. So, we we will have no duplicate elements. (alternatively call distinct on stream and then collect to list)

private static Set<Integer> collect(List<Integer> list) {
return list.stream().filter(number -> number % 2 == 0).collect(Collectors.toSet());
}

3- Collectors.toMap(..)

It returns a collector that collects the elements in a stream into a map using given key and value mappers.

private static Map<Integer, String> collect(List<String> list) {
return list.stream().collect(Collectors.toMap(String::length, String::toUpperCase));
}

In this example, we have a list of strings as a parameter and we are creating a map from it. Key will be the length of the string and value will be upper case version of the string.

KeyMapper -> String::length
ValueMapper -> String:toUpperCase

When collecting to map, there may be duplicate keys. To resolve this, we can provide merge function resolve collision.

private static Map<Integer, String> collectElementsToMapWithDuplicateKeys(List<String> list) {
return list.stream().collect(Collectors.toMap(String::length, String::toUpperCase, (s1, s2) -> s1 + " " + s2));
}

These are the three main collectors we can use to collect stream into a collection. Below, you can see other collectors similar to the ones above.

Collectors.toCollection(..) // collects to given Collection like
-
Collectors.toCollection(LinkedHashSet::new)
- Collectors.toCollection(LinkedList::new)
- Collectors.toCollection(() -> new PriorityQueue<>()); //without method reference
Collectors.toConcurrentMap(..) // same usage as toMap(..), collects to concurrentMapCollectors.toUnmodifiableList() // same usage as toList(), collects to unmodifiable listCollectors.toUnmodifiableSet() // same usage as toSet(), collects to unmodifiable setCollectors.toUnmodifiableMap(..) // same usage as toMap(..), collects to unmodifiable map

Now, as we learned the fundamental collector operations, we can now see the advanced ones.

4- Collectors.groupingBy(..)

This operation lets you group your stream elements into a map using given grouping function.

private static  Map<Integer, List<String>> groupElementsByLength(List<String> list) {
return list.stream().collect(Collectors.groupingBy(String::length));
}

We have a string list as a parameter. We are creating a stream over it and collecting it grouping by their length. So resulting Map will have keys from length and values from list of strings.

System.out.println(groupElementsByLength(Arrays.asList("abc", "bc", "cd", "def", "eghi", "f", "gij", "h", "i", "j")));// output: {1=[f, h, i, j], 2=[bc, cd], 3=[abc, def, gij], 4=[eghi]}

There is another groupingBy method that lets you specify downstream collector. We can implement the above function also using this method.

private static Map<Integer, List<String>> collect(List<String> list) {
return list.stream().collect(Collectors.groupingBy(String::length, Collectors.toList()));
}

There is another grouping method called groupingByConcurrent which serves the same purpose in concurrent manner and returns concurrentMap.

5- Collectors.joining()

This returns a collector that concatenates the strings in a stream.

private static String concatStrings(List<String> list) {
return list.stream().filter(s -> s.length() > 2).collect(Collectors.joining());
}
System.out.println(concatStrings(Arrays.asList("abc", "bc", "cd", "def", "eghi", "f", "gij", "h", "i", "j")));
// prints abc-def-eghi-gij

We are first filtering elements to eleminate strings that have lengths lower than 3 and then joining them into a String.

We can also specify a delimiter as follows.

private static String concatStringsWithDelimiter(List<String> list) {
return list.stream().filter(s -> s.length() > 2).collect(Collectors.joining("-"));
}
System.out.println(concatStringsWithDelimiter(Arrays.asList("abc", "bc", "cd", "def", "eghi", "f", "gij", "h", "i", "j"), "-"));
// prints abc-def-eghi-gij

6- Collectors.partitioningBy(..)

This collector lets you collect your stream into map of size two with keys true and false. Elements evaluating predicate true falls into true key and elements evaluating predicate false falls into false key.

private static Map<Boolean, List<String>> partitionByLength(List<String> list, int length) {
return list.stream().collect(Collectors.partitioningBy(s -> s.length() > length));
}
System.out.println(partitionByLength(Arrays.asList("abc", "bc", "cd", "def", "eghi", "f", "gij", "h", "i", "j"), 3));// prints {false=[abc, bc, cd, def, f, gij, h, i, j], true=[eghi]}

In this example, we are partitioning string list using length and our partitioning predicate is length > 3.

If you like to use another downstream collector, you can specify it in the second param. This is the same as above with explicit downstream.

private static Map<Boolean, List<String>> partitionByLengthWithExplicitDownstream(List<String> list, int length) {
return list.stream().collect(Collectors.partitioningBy(s -> s.length() > length, Collectors.toList()));
}

7- Collectors.reducing(..)

It basically reduce given stream according to given reduce operator. In the following example we are calculating sum of the numbers by Integer::sum operator.

private static Integer reduceNumbers(List<Integer> numbers, int initialIdentity) {
return numbers.stream().collect(Collectors.reducing(initialIdentity, Integer::intValue, Integer::sum));
}
System.out.println(reduceNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 0));// prints 55

8- Collectors.flatMapping(..)

This lets you flat map during collection. To make it clear, we can see the example below. Suppose, we have a list of list of int, List<List<Integer>>. We want to group lists by their sizes and as values, we want unique elements for each group of list size. We first group them using list::size operator. Then we need to flat the nested list as we want to group integers not lists. As a downstream operator, we are using toSet to eliminate duplicate values.

private static Map<Integer, Set<Integer>> groupByListSizeAndEliminateDuplicateValues(List<List<Integer>> nestedNumbers) {
return nestedNumbers.stream().collect(
Collectors.groupingBy(List::size,
Collectors.flatMapping(Collection::stream, Collectors.toSet())));
}
System.out.println(groupByListSizeAndEliminateDuplicateValues(List.of(List.of(1,2,3), List.of(3,4,5), List.of(6,7), List.of(7,8))));// prints {2=[6, 7, 8], 3=[1, 2, 3, 4, 5]}

9- Collectors.filtering(..)

This is basically filtering elements when collecting them. If the element cannot evaulate predicate true, it will not be collected. It is similar to filtering first using filter method and then collecting all.

private static List<Integer> filterEvenNumbersCollecting(List<Integer> list) {
return list.stream().collect(Collectors.filtering(number -> number % 2 == 0, Collectors.toList()));
}

This is how you can filter number list when collecting. And the below one uses filter method and is the same as above one.

private static List<Integer> filterEvenNumbers(List<Integer> list) {
return list.stream().filter(number -> number % 2 == 0).collect(Collectors.toList());
}

10- Collectors.summingInt(..), Collectors.summingDouble(..), Collectors.summingLong(..)

These collectors lets you sum elements in a stream. It takes mapper as only parameter.

private static int sumNumbers(List<Integer> list) {
return list.stream().collect(Collectors.summingInt(Integer::intValue));
}

11- Collectors.averagingInt(..), Collectors.averagingDouble(..), Collectors.averagingLong(..)

These collectors lets you find average of elements in a stream. It takes mapper as only parameter.

private static double findAverage(List<Integer> list) {
return list.stream().collect(Collectors.averagingInt(Integer::intValue));
}

12- Collectors.summarizingInt(..), Collectors.summarizingDouble(..), Collectors.summarizingLong(..)

These collectors returns some statistical information about the stream which can be of type Int, Double, Long. The data it presents are sum, average, count, min, max.

private static void summarize(List<Integer> list) {
IntSummaryStatistics intSummaryStatistics = list.stream().collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(intSummaryStatistics.toString());
}
summarize(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20));// prints IntSummaryStatistics{count=20, sum=210, min=1, average=10,500000, max=20}

13- Collectors.minBy(..), Collectors.maxBy(..)

These collectors lets you find min and max value using given comparator parameter. Let’s see the example below. It groups numbers by their digit count and as values, it puts the minimum number of that digit count group.

private static Map<Integer, Optional<Integer>> groupNumbersByDigitCountAndMinValue(List<Integer> list) {
return list.stream().collect(Collectors.groupingBy(number -> String.valueOf(number).length(), Collectors.minBy(Comparator.naturalOrder())));
}
System.out.println(groupNumbersByDigitCountAndMinValue(List.of(2, 3, 7, 14, 16, 20, 25)));// prints {1=Optional[2], 2=Optional[14]}

14- Collectors.counting()

This lets you count number of occurrances when collecting. In the example below, we get a list of integers and return a map which groups the numbers by their digit count and as values, it sets the number of elements of each digit count group.

private static Map<Integer, Long> groupNumbersByDigitCountAndCountOfAssociatedValues(List<Integer> numbers) {
return numbers.stream().collect(
Collectors.groupingBy(num -> String.valueOf(num).length(),
Collectors.counting()));
}
System.out.println(groupNumbersByDigitCountAndCountOfAssociatedValues(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)));// prints {1=9, 2=2}

15- Collectors.collectingAndThen(..)

This collector lets you apply a function to a collected stream. In this example, we are partitioning the given list by given predicate and sorting partitioned values. As we know, partitioning by partitions given list by given predicate into a map with keys true and false. We then appyling a final function to values to sort them.

private static <T> Map<Boolean, List<T>> partitionAndSum(Predicate<T> predicate, List<T> numbers) {
return numbers.stream().collect(
Collectors.partitioningBy(predicate,
Collectors.collectingAndThen(Collectors.toList(),
list -> list.stream().sorted().collect(Collectors.toList()))));
}
System.out.println(partitionAndOrderValues(num -> num % 2 == 0, Arrays.asList(11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)));// prints {false=[1, 3, 5, 7, 9, 11], true=[2, 4, 6, 8, 10]}

Note:

You probably notice that stream api already has methods that can do what some collector methods can do like

Collectors.reducing() -> stream.reduce()Collectors.filtering() -> stream.filter()

You better use the stream method when possible and use collectors version as downstream collector as you will see below.

More Examples

1- Given a Map<Integer, Map<Integer, String>>, return the same map but with nested value of type Integer which is Map<Integer, Map<Integer, Integer>>.

private static Map<Integer, Map<Integer, Integer>> convertNestedValues(Map<Integer, Map<Integer, String>> map) {
return map.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey, // outer key mapper
outerEntry -> outerEntry.getValue() // outer value mapper
.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey, // inner key mapper
innerEntry -> Integer.parseInt(innerEntry.getValue()))))); // outer value mapper
}

At first, it may look complicated but when you digest it piece by piece, you will understand what is going on. We are starting off collecting our entry by toMap collector as we need to change inner map but keep the same outer map structure. This collector lets us collect the map with given key and value mapper. We do not need to change key so we are mapping key to leave as it is. Then, we are collecting value entryset stream using toMap collectors. We are again leaving the key as it is. Here, the nested values will be our interest. We are providing a value mapper that will parse String to Integer.

2- Given a list of words, create a map whose keys are words length and whose values contain words of that length with alphabetical order.

// sample
System.out.println(test(Arrays.asList("a", "b", "ab","abc", "bcd", "abcd", "abcde", "bcdef", "abcdefg", "bcdefghi", "abcdefghij")));
// result
{1=[a, b], 2=[ab], 3=[abc, bcd], 4=[abcd], 5=[abcde, bcdef], 7=[abcdefg], 8=[bcdefghi], 10=[abcdefghij]}

We are starting by grouping words by their length using groupingBy collector. First parameter is how we are going to group them. In our case, length will be the grouping factor. Second parameter is the downstream collector and we are using collectingAndThen collector to have sorted word list. First parameter of the collectingAndThen will be toList collectors as we want to have list of words as map values. Second parameter is what we want to do with resultant word list. We want them to be in alphabetical order. For that, we are using sorted method of stream api.

private static Map<Integer, List<String>> groupWordsByLengthWithSortedValueList(List<String> words) {
return words.stream().collect(Collectors.groupingBy(
String::length,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream().sorted().collect(Collectors.toList()))));
}

3- Given a list of numbers, group numbers by their digit count and sum numbers as value associated values.

// sample
System.out.println(groupNumbersByDigitCountAndSummingValues(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)));
// result
{1=45, 2=10} // sum 1 to 9 as they are of digit count 1, leave 10 as it is as it is only one of digit count 2.

We can start by grouping them by their digit count using groupBy collector. Then, we can simply use summingInt as downstream collector to sum values of same digit count group.

private static Map<Integer, Integer> groupNumbersByDigitCountAndSummingValues(List<Integer> numbers) {
return numbers.stream().collect(
Collectors.groupingBy(num -> String.valueOf(num).length(),
Collectors.summingInt(Integer::intValue)));
}

To achieve this, we could also use reducing collector to find sum of same digit count numbers.

private static Map<Integer, Integer> groupNumbersByDigitCountAndSummingValuesReducing(List<Integer> numbers) {
return numbers.stream().collect(
Collectors.groupingBy(num -> String.valueOf(num).length(),
Collectors.reducing(0, Integer::intValue, Integer::sum)));
}

In this article, we have seen how to collect stream data in various ways. I hope you like it and find it useful. You can find the codes in the following github repo.

--

--

Ömer Kurular

I am Ömer and currently working as a full-time software engineer. I will share my knowledge with you I gained through years of professional and self working.