Search code examples
javajava-stream

Group by two fields and filter by another field value get final result in each group


I have an object structure like

public class Employee {
       private String name;
       privte String department;
       private String gender;
       private String designation;
       private Integer salary;         
}

 List<Employee> listEmployees = new ArrayList<>(List.of(
            new Employee("Mark", "Marketing", "male", "Sales Manager", 9200),
            new Employee("Tom", "Marketing", "male", "Sales Manager", 2800),
            new Employee("Travis", "Marketing", "male", "Sales Manager", 3850),
            new Employee("Diana", "Marketing", "female", "Sales Manager", 1900),
            new Employee("Keith", "R&D", "male", "Software Engineer", 4000),
            new Employee("Liam", "R&D", "male", "Software Engineer", 5200),
            new Employee("Whitney", "R&D", "female", "Software Engineer", 6000),
            new Employee("Tina", "R&D", "female", "Software Engineer", 7500)
            new Employee("Patrick", "Government", "female", "Service", 7100),
            new Employee("Luc", "Government", "male", "Service", 3300),
            new Employee("Philippe", "Government", "female", "Service", 2800),
            new Employee("David", "Government", "female", "Service", 4700)
    ));

I want result from this employee list from each department and gender who is earning highest salary.

I tried with two filed group by department and gender after that get result map inside map then list but how to filter by highest salary and get the final result from each group?

So my result would be like below.

Employee("Mark", "Marketing", "male", "Sales Manager", 9200)
Employee("Diana", "Marketing", "female", "Sales Manager", 1900)
Employee("Liam", "R&D", "male", "Software Engineer", 5200)
Employee("Tina", "R&D", "female", "Software Engineer", 7500)
Employee("Patrick", "Government", "female", "Service", 7100)
Employee("Luc", "Government", "male", "Service", 3300)

I have tried with below code:

employeeList.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.groupingBy(Employee::getGender)));

Solution

  • Using groupingBy would work but is overly complex and somewhat cumbersome to get the desired results. Assuming you had the getters defined, you could put them in a map where the key is the concatenation of the departement and gender and the value is the Employee instance. Then use a BinaryOperator as a mergeFunction to replace the existing employee if the next employee's salary is higher. The LinkedHashMap option is not required but is used to preserve the encounter order of the Employees. To finish, the values() of the map are returned and put in a List and printed.

    I am using a record for the demo.

    record Employee(String getName, String getDepartment, String getGender,
            String getDesignation, Integer getSalary) {
         @Override
         public String toString() {
             return "Employee(%s, %s, %s, %s, %d)".formatted( getName, getDepartment, getGender,
                     getDesignation,getSalary);
         }
    }
    
    • function to select two fields.
    Function<Employee, String> key = emp->emp.getDepartment()+emp.getGender();
    
    • mergeFunction for desired return.
    BinaryOperator<Employee> mergeFunction = (max, next) -> 
                max.getSalary() > next.getSalary() ? max : next;
    

    The process

    List<Employee> maxSalaries = new ArrayList<>(listEmployees.stream()
             .collect(Collectors.toMap(
                      emp->key.apply(emp), emp -> emp,
                     mergeFunction,
                     LinkedHashMap::new)).values());
    
    maxSalaries.forEach(System.out::println);
    

    New list of Employee:

    Employee(Mark, Marketing, male, Sales Manager, 9200)
    Employee(Diana, Marketing, female, Sales Manager, 1900)
    Employee(Liam, R&D, male, Software Engineer, 5200)
    Employee(Tina, R&D, female, Software Engineer, 7500)
    Employee(Patrick, Government, female, Service, 7100)
    Employee(Luc, Government, male, Service, 3300)
    
    

    Here is a version that uses nested groupingBy collectors and a reducing collector. I still prefer the first method but this may be more in line with what you want.

    Map<String, Map<String, Employee>> results2 = listEmployees.stream().collect(Collectors.groupingBy(Employee::getDepartment,
             Collectors.groupingBy(Employee::getGender,
                     Collectors.reducing(new Employee("","","","",-1), mergeFunction ))));
     
     results2.forEach((k,v) -> {
         v.values().forEach(System.out::println);
         
     });
    

    prints

    Employee(Tina, R&D, female, Software Engineer, 7500)
    Employee(Liam, R&D, male, Software Engineer, 5200)
    Employee(Patrick, Government, female, Service, 7100)
    Employee(Luc, Government, male, Service, 3300)
    Employee(Diana, Marketing, female, Sales Manager, 1900)
    Employee(Mark, Marketing, male, Sales Manager, 9200)