1

I have a list of Employee objects as follows.

public class Employee {
    int eno;
    String name;
    String dept;
    int  salary;
   ......
}

The requirement is to get the list of employee names getting the maximum salary in each department. There can be more than one employee having the same highest salary in the department. For example, the Sales team may have 2 employees, say Alex & John, earning the highest salary.

List<Employee> eList = new ArrayList<>();
eList .add(new Employee(101, "ALEX","SALES",7000));
eList.add(new Employee(102, "KATHY","ADMIN",3000));
eList.add(new Employee(103, "JOHN","SALES",7000));
eList.add(new Employee(104, "KONG","IT",5000));
eList.add(new Employee(105, "MIKE","SALES",4000));

The expected output is -> Map(DEPT, List Of Names with max salary)

With the below code, I can get only one Employee object as an output, even though there is more than one employee satisfying the condition.

Map<String, Employee> maxSalEmpNameByDept = eList.stream().collect(groupingBy(Employee::getDept, 
                                                        collectingAndThen(maxBy(Comparator.comparing(Employee::getSalary)), Optional::get)));  

This is because the MaxBy method stops when it finds the fist item that satisfies the given condition.

My questions are:

  1. How can we get the list of highest-paid Employee's (Map <String, List<Employee>>)in each department?
  2. How to retrieve the list of highest-paid employee names alone (Map <String, List<String>>) in each department?
Naman
  • 27,789
  • 26
  • 218
  • 353
  • @Holger I think liked the question partially solved this question as OP needs highest-paid employee list for **each department**. – Eklavya Sep 08 '20 at 06:53
  • 1
    @Rono Given the OP’s original code, it seems to me that there is enough understanding of the Stream API to combine a collector, like shown in the second answer to the linked question with `groupingBy`. Otherwise, they can open a new question. – Holger Sep 08 '20 at 06:57

3 Answers3

1

First, create a map for department-based max salary using Collectors.toMap

Map<String, Integer> maxSalByDept =
        eList.stream()
             .collect(Collectors.toMap(Employee::getDept, Employee::getSalary,
                         BinaryOperator.maxBy(Comparator.comparing(Function.identity()))));

Then filter the employee list by department based max salary first then grouping by the department. So you get employee of max salary of each department.

Map<String, List<Employee>> resEmpMap =
    eList.stream()
         .filter(e -> maxSalByDept.get(e.getDept()).equals(e.getSalary()))
         .collect(Collectors.groupingBy(Employee::getDept));

And to get only employee names you can use Collectors.mapping in groupingBy

Map<String, List<String>> resEmpMap =
      eList.stream()
           .filter(e -> maxSalByDept.get(e.getDept()).equals(e.getSalary()))
           .collect(Collectors.groupingBy(Employee::getDept,
                Collectors.mapping(Employee::getName, Collectors.toList())));
Eklavya
  • 17,618
  • 4
  • 28
  • 57
0

If you write your own collector, it's quite easy. I wrote this before really searching, but the library StreamEx actually has a built-in collector for this.

private static <T> Collector<T, List<T>, List<T>> allMax(Comparator<T> comparator) {
    return Collector.of(
        ArrayList::new,
        (list, item) -> {
            if (list.isEmpty()) { // if this is the first element, guaranteed to be max
                list.add(item);
                return;
            }

            int comparison = comparator.compare(item, list.get(0));
            if (comparison > 0) { // if the item is greater than the current max(s), discard it
                list.clear();
                list.add(item);
            }
            else if (comparison == 0) { // if the item is equal to the current max(s), add it
                list.add(item);
            }
        },
        // Only needed for parallel streams, implement if you want, but not required
        (listA, listB) -> { throw new UnsupportedOperationException(); } 
    );
}

Your code then becomes

Map<String, List<Employee>> collect = eList.stream()
    .collect(
        Collectors.groupingBy(
            Employee::getDept,
            allMax(Comparator.comparing(Employee::getSalary))
        )
    );

To get just the names (your question 2), you can use a collectingAndThen to map the list of employees to just their names.

Map<String, List<String>> collect = eList.stream()
    .collect(
        Collectors.groupingBy(
            Employee::getDept,
            Collectors.collectingAndThen(
                allMax(Comparator.comparing(Employee::getSalary)),
                (emps) -> emps.stream().map(Employee::getName).collect(Collectors.toList())
            )
        )
    );
Michael
  • 41,989
  • 11
  • 82
  • 128
  • 2
    [This old answer](https://stackoverflow.com/a/29339106/2711488) has a complete collector, so there’s no need to think about whether to add a merge function or not. – Holger Sep 08 '20 at 06:25
0

Here is one way without using an explicit Comparator or some third party library. I filled out your Employee class and added some additional employees.

Provided data

List<Employee> eList = new ArrayList<>();
eList.add(new Employee(101, "ALEX", "SALES", 7000));
eList.add(new Employee(102, "KATHY", "ADMIN", 3000));
eList.add(new Employee(103, "JOHN", "SALES", 7000));
eList.add(new Employee(104, "KONG", "IT", 5000));
eList.add(new Employee(106, "MIKE", "SALES", 4000));
eList.add(new Employee(107, "BOB", "IT", 4000));
eList.add(new Employee(108, "SARAH", "ADMIN", 2000));
eList.add(new Employee(109, "RALPH", "SALES", 4000));
eList.add(new Employee(110, "JUNE", "ADMIN", 3000));

The following pulls the maximum salary(s) for each department.

  • First, group first map by department.
  • Now create another map and group the employees by their salary. The salary is the key for this map. The value will be a list of employees.
  • Since the inner map is a Treemap the values are naturally sorted from low to high so the last entry has the highest salary.
  • Simply retrieve those last entries for each department and you have the highest paid employees.
Map<String, List<Employee>> highestSalaries = eList.stream()
        .collect(Collectors.groupingBy(Employee::getDept,
                Collectors.groupingBy(Employee::getSalary,
                        TreeMap::new, Collectors.toList())))
        .entrySet().stream()
        .collect(Collectors.toMap(e -> e.getKey(),
                e -> e.getValue().lastEntry().getValue()));

highestSalaries.entrySet().forEach(System.out::println);

Prints

ADMIN=[{102,  KATHY,  ADMIN,  3000}, {110,  JUNE,  ADMIN,  3000}]
IT=[{104,  KONG,  IT,  5000}]
SALES=[{101,  ALEX,  SALES,  7000}, {103,  JOHN,  SALES,  7000}]

To get just the names, use the just created map

  • stream the values(which are lists of employees)
  • then put lists of employees in one Employee stream via flatMap
  • Remember, at this point these are the highest salaries per department so just group employees by department and collect their names in a list.
Map<String, List<String>> highestSalariesNames = 
        highestSalaries.values().stream()
                .flatMap(List::stream)
                .collect(Collectors.groupingBy(
                        Employee::getDept,
                        Collectors.mapping(Employee::getName,
                                Collectors.toList())));

highestSalariesNames.entrySet().forEach(System.out::println);

Prints

IT=[KONG]
ADMIN=[KATHY, JUNE]
SALES=[ALEX, JOHN]

Your class I modified with constructor, getters, and toString.

    public static class Employee {
        private int eno;
        private String name;
        private String dept;
        private int salary;
        
        public Employee(int eno, String name, String dept,
                int salary) {
            this.eno = eno;
            this.name = name;
            this.dept = dept;
            this.salary = salary;
        }
        
        public int getEno() {
            return eno;
        }
        
        public String getName() {
            return name;
        }
        
        public String getDept() {
            return dept;
        }
        
        public int getSalary() {
            return salary;
        }
        
        @Override
        public String toString() {
            return String.format("{%s,  %s,  %s,  %s}", eno, name,
                    dept, salary);
        }
    }
    
}
WJS
  • 36,363
  • 4
  • 24
  • 39
  • After nested groupingBy you can this way to avoid flatMaping `...entrySet().stream().collect(Collectors.toMap(m -> m.getKey(), m -> m.getValue().lastEntry().getValue()));` – Eklavya Sep 08 '20 at 05:52
  • 1
    @Rono Excellent observation. I made the change. Thanks! – WJS Sep 08 '20 at 14:31