Core Java

Java 8 Stream Collectors

Introduction:

Java 8 Streams is a powerful feature introduced in Java 8. To learn more about Streams in Java, we’ll suggest you read this post.

In this tutorial, we’ll cover how to collect the output stream into one of the known Collection types followed by exploring a few other Collectors options.

We’ll be reusing the below lists in almost all of our examples:

List<Integer> listOfMarks = Arrays.asList(89, 98, 90, 75);

List<Student> listOfStudents = new ArrayList<>();
listOfStudents.add(new Student(1, "Maths"));
listOfStudents.add(new Student(2, "Science"));
listOfStudents.add(new Student(3, "Arts"));

Here we assume, the Student class has two attributes: id and courseName.

collect() method && Collectors class:

collect() method is a terminal operation provided over a Stream to collect the output stream into one of the defined Collection types. It takes a Collector type as an input. TheĀ  java.util.stream.Collectors class is the one which provides the implementation of different Collector types.

Collecting Stream in a List, Set or Map:

1.) Collectors.toList():

To collect any stream into a List, we’ll use Collectors.toList():

List<Integer> listOfMarksGt90 = listOfMarks.stream().filter(m -> m > 90)
                                  .collect(Collectors.toList());

The above code returns a List<Integer> holding all marks value greater than 90.

2.) Collectors.toSet():

To collect our stream into a Set instance, we’ll be using Collectors.toSet():

Set<Integer> setOfMarksGt90 = listOfMarks.stream().filter(m -> m > 90)
                                .collect(Collectors.toSet());
3.) Collectors.toCollection():

When using Collectors.toList() or Collectors.toSet(), we can by no means specify a type of a Set or List implementation. In cases when we feel the need to do so, we can use Collectors.toCollection():

LinkedList<Integer> linkedListOfMarksGt90 = listOfMarks.stream().filter(m -> m > 90)
                                              .collect(Collectors
                                              .toCollection(LinkedList::new));

However, it’s important to note that it will only work for mutable types of Collection implementation.

4.) Collectors.toMap():

To collect a stream into a Map would make use of Collectors.toMap() implementation. It has three variants:

static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
                                             Function<? super T,? extends U> valueMapper)

static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
                                             Function<? super T,? extends U> valueMapper,
                                             BinaryOperator<U> mergeFunction)

static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
                                             Function<? super T,? extends U> valueMapper,
                                             BinaryOperator<U> mergeFunction,
                                             Supplier<M> mapSupplier)

Let’s first understand each of the parameters used:

  1. keyMapper – A mapping function used to produce the keys of a Map.
  2. valueMapper – A mapping function to produce the values within a Map.
  3. mergeFunction – When two or more values resolve to the same key, this binary operator is used to resolve the conflict.
  4. mapSupplier – A function which specifies an implementation of Map to use as an outcome, say for instance a LinkedHashMap or a TreeMap.

To understand it further, let’s try out examples of each of these flavors:

//Output- {1=Maths, 2=Science, 3=Arts}
Map<Integer, String> map = listOfStudents.stream()
                             .collect(Collectors.toMap(Student::getId, Student::getCourseName));

//In cases, where same student can enroll in multiple cases, we need to specify merger function
map = listOfStudents.stream()
        .collect(Collectors.toMap(Student::getId, Student::getCourseName, (c1,c2) -> c1+","+c2));

//Below code returns a LinkedHashMap as an output - retaining insertion order
map = listOfStudents.stream()
         .collect(Collectors.toMap(Student::getId, Student::getCourseName, (c1,c2) -> c1+","+c2, LinkedHashMap::new));

We must always provide a merger function whenever we expect two or more values being mapped to the same key. For example, if one student can enroll in multiple courses, then we need to provide a merger function for the conflict resolution.

 

Other Collectors:

Apart from collecting our Streams into a Collection, we can also choose to perform some advanced operations. java.util.stream.Collectors is a pretty huge library, we’ll cover only a few of those:

1.) Collectors.joining():

It helps us join all the values in a Stream<String>. We can optionally choose to provide a separator as well. Let’s see an example:

//allCourseNames becomes "Maths,Science,Arts"
String allCourseNames = listOfStudents.stream()
                           .map(Student::getCourseName)
                           .collect(Collectors.joining(","));

The value of allCourseNames will be “Maths,Science,Arts on executing the above code.

We can also optionally provide a prefix and suffix to the resulting String:

//allCourseNames becomes "[Maths,Science,Arts]"
String allCourseNames = listOfStudents.stream()
                           .map(Student::getCourseName)
                           .collect(Collectors.joining(",", "[", "]"));

 

2.) Collectors.maxBy/minBy():

maxBy() and minBy() Collectors helps us find the maximum and the minimum value in a Stream respectively. Let’s look at an example implementation:

Optional<Integer> maxMarks = listOfMarks.stream()
                               .collect(Collectors.maxBy(Integer::compare));
Optional<Integer> minMarks = listOfMarks.stream()
                               .collect(Collectors.minBy(Integer::compare));

Both of these methods take a Comparator as an argument.

3.) Collectors.summingInt()/summingLong()/summingDouble():

As the name suggests, summingInt() sums up the integer values, summingLong() and summingDouble() are responsible for summing up long & double values respectively.

Let’s try out a simple example:

int totalMarks = listOfMarks.stream() 
                   .collect(Collectors.summingInt(n -> n));
4.) Collectors.averagingInt()/averagingLong()/averagingDouble():

It helps us find out the average of numbers in a given stream. Let’s consider an example:

double avgMarks = listOfMarks.stream() 
                   .collect(Collectors.averagingInt(n -> n));

Conclusion:

We started this tutorial by looking at how Collectors can help us to collect our output stream into a List, Set or Map. We later covered a few more advanced Collectors available in the API.

Be the First to comment.

Leave a Comment

Your email address will not be published. Required fields are marked *