Guide to Java 8 Collectors

In this tutorial, we will go through Java 8 Collectors. We use these Java collectors at the final step of processing a Java stream.

 

Introduction

Java 8 Collectors class extends Object class and implements the Collector interface. It provides different methods to operate on stream elements. In this tutorial, we will learn different use cases of Java 8 Collectors.

 

1. Collectors.toList

Using this method, we can collect all Stream elements to create a List.toList() is a static method and we can’t say what List implementation this method returns. This method can’t guarantee what type of list it will return, its mutability, serializability or any information about the thread safety of the list. It collects the elements in encounter order.

We recommend the toCollection method to use if we want more control over the returning list. For example:

System.out.println(Stream.of(4,3,2,7,1).collect(Collectors.toList()));

This will print the list [4, 3, 2, 7, 1] as output.

 

2. Collectors.toSet

This method is like the above one. The only difference is that we can use it to create a new set from the Stream elements. It will return one collector that will collect the elements to create a new Set. Similar to the toList method, we can’t guarantee anything about the returning set, mutability, serializability or thread safety. We can use the toCollection method to get more control over the return set.

System.out.println(Stream.of(1,7,9,2,3,1).collect(Collectors.toSet()));

This will print [1, 2, 3, 7, 9]. As you can see that the element ‘1’ is included only once as it is a set and it can’t hold similar elements. Also, the elements are sorted unlike the previous one where elements are collected in encountered order.

 

3. Collectors.toCollection

The toCollection method is useful if we want to create a collection from stream elements. We can use this method instead of toList or toSet if we want more control over the returning collection.

Stream stream = Stream.of(1,7,9,2,3,1);
Set set = stream.collect(Collectors.toCollection(TreeSet::new));
System.out.println(set);

 

4. toMap and toConcurrentMap

We use this method to create a new Map. It returns one Collector that collects the elements of the stream to a Map. We calculate the key-value pairs using two mapping functions passed as parameters. Following are the parameters we can use:

  1. keyMapper: Mapping function for producing keys of the Map
  2. valueMapper: Mapping function for producing values of the Map
  3. mergeFunction: If we find any same key, this function will help to resolve that collision.
  4. mapSupplier: Function that returns a new empty map with the result values.

This function has three variants. It can either take (keyMapper, valueMapper), (keyMapper, valueMapper, mergeFunction) or (keyMapper,valueMapper,mergeFunction, mapSupplier).

Stream  wordStream = Stream.of("one", "hello", "opera", "hi","music");
 HashMap <Character, String> m = wordStream.collect(Collectors.toMap(word -> word.charAt(0),
   word -> word,
   (word1, word2) -> word1 + "," + word2,
   HashMap::new));
 System.out.println(m);

Output:

{h=hello,hi, m=music, o=one,opera}

Here, the key for the Map is the starting character of each string. If two strings start with the same character, the value of that key will be the concatenation of both strings with “,” as the separator. The return value is HashMap.

 

5. Collectors’s  summingInt,summingLong and summingDouble

The Collectors class provides three different methods for quickly finding out the sum of all string elements. We have three different methods to calculate the sum of integer values, sum of long values and sum of doubles. It calculates the sum based on an integer-valued, long-valued or double-valued function. For doubles, the sum may vary based on the order in which it collects the elements. The result will be 0 if no elements are present.

Let me show you with an example:

Stream intStream = Stream.of(1,2,3,4,5);
System.out.println(intStream.collect(Collectors.summingInt(Integer::intValue)));

It will print '15', i.e. the sum of 1,2,3,4,and 5. As you can see that this method takes one mapper function to determine the property to consider. In a similar way, we can find out the sum of long or double elements.

 

6. Collectors’s  averagingInt,averagingLong and averagingDouble

We use the averagingInt,averagingLong and averagingDouble methods of Collectors to find out the average value of a series of integer, long or double values. If it finds no elements, it will return 0.

Stream intStream = Stream.of(5,5,4,6,4);
System.out.println(intStream.collect(Collectors.averagingInt(Integer::intValue)));

It will return the average or arithmetic mean of the numbers, i.e. (5+5+4+6+4)/5 or 4.8.Similarly, we can find the average of long or double elements of a stream.

 

7. counting, maxBy, minBy

We use the counting() method to find the total number of elements.

Stream strStream = Stream.of("1","2","3","4","5");
System.out.println(strStream.collect(Collectors.counting()));

It will print 5.We can use maxBy() and minBy() methods to find the maximum and minimum elements of a stream. It takes one Comparator to decide.

Stream intStream = Stream.of(4,2,6,12,33,44);
System.out.println(intStream.collect(Collectors.maxBy(Comparator.naturalOrder())));

The return value is an ‘Optional’. It will print Optional[44].

 

8. Collectors’s groupingBy and partitioningBy

The groupingBy method is used to group stream elements with a specific property. We can pass one property, usually a lambda function, that is used to group the strings. The below example will group the string elements by their length:

Stream strStream = Stream.of("one","two","b","d","three");
System.out.println(strStream.collect(Collectors.groupingBy(String::length)));

The output is "{1=[b, d], 3=[one, two], 5=[three]}". The partitioningBy takes one Predicate as a parameter. It organizes them as a Map with boolean values as key and Collections as a value. It places all elements that matches under the ‘true’ key and places other elements under 'false' key.

Stream intStream = Stream.of(1, 4, 5, 2, 7);
System.out.println(intStream.collect(Collectors.partitioningBy(e -> e % 2 == 0)));

The output is: {false=[1, 5, 7], true=[4, 2]}. It places all even numbers placed under ‘true’ key and other numbers under 'false' key.

 

9. Collectors’s summarizingInt,summarizingDouble and summarizingLong

Collectors class comes with three different static methods summarizingInt,summarizingDouble and summarizingLong to find out the statistical summary of the collected elements. These functions take one mapping function to apply on each extracted elements.

Stream intStream = Stream.of(5, 4, 3, 2, 1);
Map<Integer, IntSummaryStatistics> map = intStream.collect(Collectors.groupingBy(Integer::intValue, Collectors.summarizingInt(Integer::intValue)));
System.out.println(map);

Output:

{1=IntSummaryStatistics{count=1, sum=1, min=1, average=1.000000, max=1}, 
2=IntSummaryStatistics{count=1, sum=2, min=2, average=2.000000, max=2}, 
3=IntSummaryStatistics{count=1, sum=3, min=3, average=3.000000, max=3}, 
4=IntSummaryStatistics{count=1, sum=4, min=4, average=4.000000, max=4}, 
5=IntSummaryStatistics{count=1, sum=5, min=5, average=5.000000, max=5}}

 

10. The joining() Method

The joining() method is used to group all elements to a string. It returns one collector that joins all elements to a string.

Stream strStream = Stream.of("one","two","three","four","five");
System.out.println(strStream.collect(Collectors.joining()));

The result of the above program is "onetwothreefourfive".The elements are collected in encounter order. The joining() method has two more variants:

a) It can take one CharSequence as the parameter to use it as a delimiter between each element of the final string.

Stream strStream = Stream.of("one","two","three","four","five");
System.out.println(strStream.collect(Collectors.joining("-")));

It will print “one-two-three-four-five“.

b) We can also add one ‘prefix’ and one ‘postfix’ to the string by passing two CharSequence as the second and the third parameter.

Stream strStream = Stream.of("one","two","three","four","five"); System.out.println(strStream.collect(Collectors.joining("-","start:",":end")));

.It will print “start:one-two-three-four-five:end“.

 

11. Collectors’s mapping,flatMapping and reducing

Mapping is useful for multi-level reduction. For example, we can use it with a groupingBy operation like below :

Stream strStream = Stream.of("one","two","three","four");
System.out.println(strStream.collect(Collectors.groupingBy(String::length,Collectors.mapping(String::toUpperCase,Collectors.toSet()))));

This program will group all strings of equal lengths and change the characters to uppercase for all strings. The output is {3=[ONE, TWO], 4=[FOUR], 5=[THREE]}.The flatMapping is like the mapping function. It takes a stream of elements accumulated by the collector. The reducing() method is used to perform a reduction on the elements using a provided binary operator.

Stream strStream = Stream.of("one","two","three","four");
System.out.println(strStream.collect(Collectors.reducing((str1,str2) -> str1 + str2)).orElse("-"));

The output is:onetwothreefour

 

12.The collectingAndThen method

The collectingAndThen, as its name suggests, is used to collect the elements of a stream and then apply a function to the final collector. We use this method for adding a transformation to the collector.

Stream strStream = Stream.of("sunday","monday","tuesday","wednesday");
List strList = strStream.collect(Collectors.collectingAndThen(Collectors.toList(),Collections::unmodifiableList));

It will collect the elements to a new list and then make the list immutable.

 

13. Custom Collector

We can also create one custom collector by implementing the Collector interface which is defined as below:

public interface Collector<T, A, R> {
   Supplier<a> supplier();
   BiConsumer<A, T> accumulator();
   BinaryOperator</a><a> combiner();
   Function<A, R> finisher();
   Set characteristics();
}</a>
  1. T – It’s the object to collect.
  2. A – It is the accumulator.
  3. R – It is the result returned by the collector.
  4. supplier() – It returns one function that is the supplier of an empty accumulator.
  5. accumulator() – Returns one function that performs the reduction operation.
  6. combiner() -Returns one function to define how to merge two accumulators when the parts of the stream are combined.
  7. finisher() – It returns one function to create the result object.
  8. characteristics – It returns one Collector. Characteristics to show the characteristic of the collector or it defines the reduction process type.

To learn how to implement a custom collector, let’s look at the below example:

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;

import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH;

class Student {
 int marks;

 Student() {}

 public Student(int i) {
  this.marks = i;
 }

 Student addMarks(Student s) {
  return new Student(this.marks = this.marks + s.marks);
 }

}

class StudentCollector implements Collector < Student, Student, Student > {
 @Override
 public Supplier < Student > supplier() {
  return Student::new;
 }

 @Override
 public BiConsumer < Student,
 Student > accumulator() {
  return Student::addMarks;
 }

 @Override
 public BinaryOperator < Student > combiner() {
  return Student::addMarks;
 }

 @Override
 public Function < Student,
 Student > finisher() {
  return Function.identity();
 }

 @Override
 public Set < Characteristics > characteristics() {
  return EnumSet.of(IDENTITY_FINISH);
 }
}
class Example {
 public static void main(String[] args) {
  Stream < Student > studentStream = Stream.of(new Student(30), new Student(40), new Student(50), new Student(80));
  System.out.println(studentStream.collect(new StudentCollector()).marks);

 }
}

The custom-collector used in this example is ‘StudentCollector’ and the custom class is ‘Student’. We are using this custom collector to find out the total sum of all student marks. If you run this program, it will print ‘200’ as the output.

 

Summary

We have learned different ways to use Collectors methods in Java 8. We have also learned how to create a custom Collectors class. Collectors class is useful to perform different operations over the Stream elements easily.