Pages

Wednesday, September 8, 2021

Java 8 Stream.collect() examples

Java 8 Stream interface provides a method called collect() which helps to perform various kind of manipulations of the elements in a Stream. These include

  • Combining all the elements of a Stream into a different data structures like List and Set.
  • Reducing and summarizing operations like count, max, min.
  • Grouping and partitioning.

The arguments passed into the collect() method are called collectors which are implementations of Collector interface.

In this tutorial, I am going to show you that, how to use these collectors under each category with examples. Very famous example, Employee and Department and the sample data set is as follows.

public class Employee {
 
    private String name;
 
    private Integer age;
 
    private String city;
 
    private Integer salary;
 
    private String sex;
 
    private Department department;
 
    public Employee(String name, Integer age, String city, Integer salary, String sex, Department department) {
        super();
        this.name = name;
        this.age = age;
        this.city = city;
        this.salary = salary;
        this.sex = sex;
        this.department = department;
    }

   // getters and setters
 
}


public class Department {

    private String departmentName;
 
    private Integer noOfEmployees;

    public Department(String departmentName, Integer noOfEmployees) {
         super();
         this.departmentName = departmentName;
         this.noOfEmployees = noOfEmployees;
    }
    // getters and setters
}


Sample data set :
Department account = new Department("Account", 75); 
Department hr = new Department("HR", 50);
Department ops = new Department("OP", 25);
Department tech = new Department("Tech", 150);
  
List<Employee> employeeList = Arrays.asList(new  Employee("David", 32, "Matara", 2000, "Male", account), 
    new  Employee("Brayan", 25, "Galle",  3000, "Male",hr),
    new  Employee("JoAnne", 45, "Negombo",  800, "Female", ops),
    new  Employee("Jake", 65, "Galle",  2500, "Male", hr),
    new  Employee("Brent", 55, "Matara",  8000, "Male", hr),
    new  Employee("Allice", 23, "Matara",  600, "Female", ops),
    new  Employee("Austin", 30, "Negombo",  9000, "Male", tech),
    new  Employee("Gerry", 29, "Matara",  2500, "Male", tech),
    new  Employee("Scote", 20, "Negombo",  2500, "Male", ops),
    new  Employee("Branden", 32, "Matara",  2500, "Male", account),
    new  Employee("Iflias", 31, "Galle",  2900, "Female", hr)); 



Combining Elements

The most straightforward and frequently used collector is toList() static method. By using this method, elements of a Stream can be combined to a List as follows.

List<Employee> employeeList = employeeList.stream().collect(Collectors.toList()); 

Similarly, if you want to convert Stream into a set, the toSet() static method can be used as follows.

Set<Employee> employeeSet = employeeList.stream().collect(Collectors.toSet());


Reducing and Summarizing


The collectors which are parameters to the Stream's collect() method can also be used to combine all the items in the stream into a single result like total, min, max and count.

Using Collectors.counting() :

If we want to find the number of employees in the list, Collectors.counting() method can be passed as an argument to collect() method as follows.
Long totalNumOfEmployees = employeeList.stream().collect(Collectors.counting());

Using Collectors.maxBy() and Collectors.minBy() :

If we want to find the maximum and the minimum value in a stream, we can use maxBy() and minBy() methods. These two collectors take a Comparator as an argument to compare the elements in the stream.

Let's try to find the employee who is getting the maximum and the minimum salary.

//Find max salary employee 
employeeList.stream() 
            .collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))) 
            .ifPresent(e -> System.out.println("Max salary employee :" + e.getName())); 

//Find min Salary 
employeeList.stream() 
            .collect(Collectors.minBy(Comparator.comparingInt(Employee::getSalary))) 
            .ifPresent(e -> System.out.println("Min salary employee :" + e.getName()));


Using Collectors.summingXXX() methods:


Collectors.summingInt(), Collectors.summingLong() and Collectors.summingDouble() are some collectors which can be used to get the summation of a particular properly of elements in a stream. These methods accept a property of corresponding type in order to perform the summing operation.

Let's say, we want to find the total salary of all the employees.

Integer totalSalary = employeeList.stream().collect(Collectors.summingInt(Employee::getSalary)); 



Using Collectors.averagingXXX() methods:

Collectors.averagingInt(), Collectors.averagingDouble() and Collectors.averagingLong() are the collectors that can be used to calculate the average value of some numeric fields. Let's say, we want to find the average salary of all the employees.

Double averageSalary = employeeList.stream().collect(Collectors.averagingInt(Employee::getSalary));

Using Collectors.summarizingXXX() methods:

Quite often, we might need to get the count, total, maximum, minimum and average by using a single operation. Collectors.summarizingInt(), Collectors.summarizingDouble() and Collectors.summarizingLong() collectors can be used to get above all statistics from a single operation. 

Let's try to get all above salary statistics from a single operation.

IntSummaryStatistics salalryStat = employeeList.stream().collect(Collectors.summarizingInt(Employee::getSalary));

IntSummaryStatistics class has many defined methods which returns the above statistic values separately. The string representation of above IntSummaryStatistics result will be as follows.

IntSummaryStatistics{count=11, sum=36300, min=600, average=3300.000000, max=9000}

Using Collectors.joining() method:

The joining() method can be used to concatenate String type property values into a single string from the elements of a stream. Note that the joining() method internally makes use of a StringBuilder to append the generated string into one.


Let's say, we want to get all the employee's names as comma separated single string.

String employeeNameStrings = employeeList.stream().map(Employee::getName).collect(Collectors.joining(", ")); 
System.out.println(employeeNameStrings);

Using Collectors.reducing() method:

The reducing() method is a generalization of all the collector methods. Using the reducing() method, we can perform many operations that were performed using specific collector methods. These operations include max, min, sum etc. 

The reducing() method takes three arguments.


  • The first argument is the starting value for reduction operation. This value will return in case empty stream. 
  • The second argument is usual transformation function. 
  • The third argument is a BinaryOperator that aggregates two arguments into a single one. 


Let's try to get the maximum and minimum salary of employees by using reducing() method too.

//Max salary
Integer maxSalary = employeeList.stream().collect(Collectors.reducing(0, Employee::getSalary, Integer::max)); 

Optional minSalary = employeeList.stream().collect(Collectors.reducing((e1, e2) -> e1.getSalary() < e2.getSalary() ? e1 :  e2));
System.out.println("Min salary " + minSalary.get().getSalary()); 

The reducing() method has a form of accepting single argument as in the above min operation.


Grouping

Groping of elements based on a one or more property is a very common database operation. The Collectors.groupingBy() factory method in Java 8 can be used to perform same kind of operations with collections of elements. The Collectors.groupingBy() method accepts a method reference as an argument which is used to classify the elements of the stream into different groups. 

Let's try to group list of employees by their department.

Map<Department, List<Employee>> employeesOverDepartment = employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment));

We call the above Function a classification function, because it is used to classify the elements of the stream into different groups. But, it is not always possible to use a classification function, because we may wish to classify the elements using more complex criteria than a simple property reference. 

The Collectors.groupingBy() method accepts a lambda expression which allows us to group elements based on more complex criteria. 

Let's say, we want to group our employee list into two groups as "High Salary" and "Low Salary". This might not be a good example, but just to show you, how we can use more complex criteria to group elements.

Map<String, List<Employee>> map = employeeList.stream().collect(
                                        Collectors.groupingBy((Employee e) -> {
                                                if (e.getSalary() > 2000) {
                                                   return "High Salary";
                                                } else {
                                                    return "Low Salary";
                                                } 
                                        }));

The similar approach can be used to group employees by their department name.

Map<String, List<Employee>> employeesByDepartmentName = employeeList.stream()
                       .collect( Collectors.groupingBy((Employee e) -> { 
                                    return e.getDepartment().getDepartmentName();
                               }));


The Collectors.groupingBy() method has a version of accepting two argument which allows us to perform multilevel grouping. In the above example, we grouped employees by the department name. 

Let's say, we want to further group employees within a particular department by their sex.

Map<String, Map<String, List<Employee>>> employeesByDepartmentName = employeeList.stream()
                .collect( Collectors.groupingBy((Employee e) -> { return e.getDepartment().getDepartmentName(); },
                         Collectors.groupingBy((Employee e) -> { return e.getSex(); }) ) );


In the above example, two-argument version of Collectors.groupingBy() method, the second argument was a another groupingBy() method. But, it is not necessarily to be a groupingBy() method. But, more generally, the second collector passed to the first groupingBy() method can be any type of collector.

Let's say, we want to get the number of employees in each department.

Map<String, Long> noOfEmployeesInDepartment = employeeList.stream()
                       .collect(Collectors.groupingBy(
                                 (Employee e) -> { return e.getDepartment().getDepartmentName(); }, 
                                  Collectors.counting()));
Using a similar kind of approach, we can find the employee who gets the maximum salary for each department. See the following example.
Map<String, Employee> maxSalaryEmployeeOfDept = employeeList.stream()
                 .collect(Collectors.groupingBy((Employee e) -> {return e.getDepartment().getDepartmentName();}, 
                          Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)), 
                                                                        Optional::get) ) );




More generally, the collector passed as the second argument to the groupingBy() factory method will be used to perform a further reduction operation on all the elements in the stream classified into the same group. Let's say, we want to get the total salary for each department.
Map<String, Integer> totalSalaryOverDepartment = employeeList.stream() 
            .collect(Collectors.groupingBy(
                         (Employee e) -> {return e.getDepartment().getDepartmentName();}, 
                          Collectors.summingInt(Employee::getSalary)));

Partitioning

0 comments:

Post a Comment

Share

Widgets