Introduction to the Collectors class

  • Last Updated: April 10, 2024
  • By: javahandson
  • Series
img

Introduction to the Collectors class

This article is about the introduction to the collectors class and how it is better than the reduce method. We will also learn about the principles of Collectors API.

Collectors class is a part of java.util.stream package. Using Collectors we can perform mutable fold operations on the data elements of a stream such as accumulating elements into collections, summarizing elements according to various criteria, grouping and partitioning the elements, etc in a very readable and safe way.

The term “mutable fold operations” refers to specific kind of operations that combines elements of a stream to produce a result, where the result container is being modified during the process. In other words, mutable fold operations modify the current result rather than creating a new result every time. This approach can be more efficient for certain operations because this process won’t create new objects for each intermediate step.

Collectors help to transform the elements of the stream into something concrete usually a collection like a List, Set, or Map. Collectors can also be used to aggregate elements into a single value like summing, finding max and min numbers, concatenating the streams, etc.

Collectors are a form of reduce method that makes things easier for developers. We don’t have to write any complex piece of code like the reduce method which we have done earlier.

Imperative code vs. collectors

In this section, we will try to understand how the functional approach using Collectors is better than an imperative way of coding.

Consider a scenario where we have a list of Students and we want to group them based on their Grades. This is a simple use case but in the imperative approach, it is quite cumbersome to implement it. Let us check the code below.

Write a program to group the Students by Grade using an imperative approach.

package com.javahands.collectors;

public class Student {

    int rollNumber;
    String name;
    char grade;

    public Student(int rollNumber, String name, char grade)
    {
        this.rollNumber = rollNumber;
        this.name = name;
        this.grade = grade;
    }

    public int getRollNumber() {
        return rollNumber;
    }

    public String getName() {
        return name;
    }

    public char getGrade() {
        return grade;
    }
}
package com.javahands.collectors;

import java.util.ArrayList;
import java.util.List;

public class GenerateStudentList {

    List<Student> getStudentList() {

        Student student1 = new Student(101, "Suraj", 'B');
        Student student2 = new Student(102, "Iqbal", 'A');
        Student student3 = new Student(103, "Amar", 'B');
        Student student4 = new Student(104, "Amit", 'C');
        Student student5 = new Student(105, "Suchit", 'C');
        Student student6 = new Student(106, "Kartik", 'A');
        Student student7 = student1;

        List<Student> studentList = new ArrayList();

        studentList.add(student1);
        studentList.add(student2);
        studentList.add(student3);
        studentList.add(student4);
        studentList.add(student5);
        studentList.add(student6);
        studentList.add(student7);

        return studentList;
    }
}
package com.javahands.collectors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {

        GenerateStudentList generateStudentList = new GenerateStudentList();
        List<Student> studentList = generateStudentList.getStudentList();

        // Grouping the students by Grade
        Map<Character, List<Student>> studentsByGradeMap = new HashMap<>();

        for (Student student : studentList) {
            List<Student> studentsForGradeList = studentsByGradeMap.get(student.getGrade());
            if (studentsForGradeList == null) {
                studentsForGradeList = new ArrayList<>();
                studentsByGradeMap.put(student.getGrade(), studentsForGradeList);
            }
            studentsForGradeList.add(student);
        }

        // Iterating over the map to print the details
        for (Map.Entry<Character, List<Student>> entry : studentsByGradeMap.entrySet()) {
            Character grade = entry.getKey();
            List<Student> students = entry.getValue();
            System.out.println("Grade : " + grade);
            for (Student student : students) {
                System.out.println("    Student Details : " + student.getRollNumber() + " " + student.getName() + " "+ student.getGrade());
            }
        }
    }
}
Output:
Grade : A
    Student Details : 102 Iqbal A
    Student Details : 106 Kartik A
Grade : B
    Student Details : 101 Suraj B
    Student Details : 103 Amar B
    Student Details : 101 Suraj B
Grade : C
    Student Details : 104 Amit C
    Student Details : 105 Suchit C

In the above example, we have successfully grouped the students by grade but we have to admit that it’s a lot of code for a simple task, also the code is harder to read. If we just look at the code we will not come to know what we are trying to achieve even though it can be expressed in simple plain English i.e. “Group a list of students by their grade”.

Now we will try to achieve the same using Collectors API and you will find the difference in how easy it is to write the below code and how readable it is.

Write a program to group the Students by Grade using Collectors API.

package com.javahands.collectors;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CollectorsEx {

    public static void main(String[] args) {
        GenerateStudentList generateStudentList = new GenerateStudentList();

        List<Student> studentList = generateStudentList.getStudentList();
        Map<Character, List<Student>> studentsByGradeMap =
                studentList.stream().collect(Collectors.groupingBy(Student::getGrade));

        // Iterating over the map to print the details
        for (Map.Entry<Character, List<Student>> entry : studentsByGradeMap.entrySet()) {

            Character grade = entry.getKey();
            List<Student> students = entry.getValue();

            System.out.println("Grade : " + grade);
            for (Student student : students) {
                System.out.println("    Student Details : " + student.getRollNumber() + " " + student.getName() + " "+ student.getGrade());
            }
        }
    }
}
Output:
Grade : A
    Student Details : 102 Iqbal A
    Student Details : 106 Kartik A
Grade : B
    Student Details : 101 Suraj B
    Student Details : 103 Amar B
    Student Details : 101 Suraj B
Grade : C
    Student Details : 104 Amit C
    Student Details : 105 Suchit C

You don’t have to understand the above code as of now, we will learn about different Collectors methods in one of the other articles but from the above code what we can understand is the code is quite readable. We just have to formulate the result we want to obtain, we don’t have to write the steps we need to perform, the Collectors class methods perform those steps internally.

The above use case was quite simple still the imperative approach looks difficult. Suppose we want to add more groupings in the above imperative code then the code will become even more harder to read and maintain. Also, the code will be too difficult to modify as it will have many nested loops and conditions.

So using the Collectors API will be better as we don’t have to deal with nested loops and conditions. Also, the code will be much more clean and readable.

Principles of Collectors API

Composability and reusability are the 2 principles of Collectors API that are used to create readable, modular, maintainable, and efficient code.

Composability

Composability means combining multiple simple components to create a more complex operation.

For Ex. The Collectors API provides operations such as toList(), toSet(), and toMap() to transform the elements of a stream into a concrete value. Also, it provides more complex collectors like groupingBy(), partitioningBy(), counting(), etc to perform more complex operations. We can combine such collector operations to achieve the desired output.

Map<Character, Set<Student>> studentsByGrade = studentList.stream()
        .collect(Collectors.groupingBy(Student::getGrade, Collectors.toSet()));

Output:

Grade : A
    Student Details set : 102 Iqbal A
    Student Details set : 106 Kartik A
Grade : B
    Student Details set : 101 Suraj B
    Student Details set : 103 Amar B
Grade : C
    Student Details set : 105 Suchit C
    Student Details set : 104 Amit C

In the above example, we are first grouping the students by grade and then collecting the students in a Set collection. So we are combining 2 operations to get a final result. In the Set collection, the duplicate elements will be eliminated hence we will not find a duplicate entry for Student Suraj.

Reusability

Reusability means creating a piece of code that can be reused across different parts of an application. Using the Collectors class we can define our custom collectors encapsulating different Collectors operations which can then be reused across different stream processing tasks.

In the below code, we will use Collectors API to create a method that returns a Collector filtering the numbers based on a given predicate.

package com.javahands.collectors;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;

public class ReusableCollectors
{
    public static <T> Collector<T, ?, List<T>> filterNumbers(Predicate<T> predicate) {
        return Collectors.collectingAndThen(Collectors.toList(),
                list -> list.stream()
                        .filter(predicate)
                        .collect(Collectors.toList())
        );
    }

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        List<Integer> evenNumbers = numbers.stream()
                .collect(ReusableCollectors.filterNumbers(n -> n % 2 == 0));
        System.out.println("Filter even numbers : "+ evenNumbers);
        
        List<Integer> oddNumbers = numbers.stream()
                .collect(ReusableCollectors.filterNumbers(n -> n % 2 != 0));

        System.out.println("Filter odd numbers : "+ oddNumbers);
    }
}
Output: 
Filter even numbers : [2, 4, 6, 8, 10]
Filter odd numbers : [1, 3, 5, 7, 9]

In the above codebase, we are reusing the same piece of code to filter the even numbers as well as odd numbers.

To summarize, both composability and reusability in the Collectors API enhance the power and flexibility of the Stream API, allowing developers to write concise, efficient, and maintainable code for data processing tasks.

In the next articles, we will learn about different Collectors class methods that are provided by Java.

So this is all about the introduction to the Collectors class in Java 8. If you have any questions on this topic please raise them in the comments section. If you liked this article then please share this post with your friends and colleagues, so that they can learn about Java and its frameworks in more depth.

Leave a Comment