Aggregate multiple fields grouping by multiple fields in Java 8

1.6k Views Asked by At

In the Employee class below, I would like to get average salary, average bonus and average perks for all employees grouping by department, designation and gender and would like the result to be a List<Employee> with the aggregated values for salary, bonus and perks.

public class Employee {

    private String name;

    privte String department;

    private String gender;

    private String designation;

    private Integer salary;

    private Integer bonus;

    private Integer perks;
    
}

What would be a clean way of doing that?

2

There are 2 best solutions below

0
On BEST ANSWER

You can do this by creating a class for the grouping key and writing a collector:

I'm simply ading the values per key and count the occurances in a map. In the finisher I devide the sums through the count.

You could get rid of the countMap by sublassing Employee, adding the count and using this class for the supplier/subtotal and using some casting...

You could also make to groupBys one for the sum and another for the count and computing the avarages with the two created maps...

public class Employee {

    private String name;

    private String department;

    private String gender;

    private String designation;

    private Integer salary;

    private Integer bonus;

    private Integer perks;

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public String getDepartment()
    {
        return department;
    }

    public void setDepartment(String department)
    {
        this.department = department;
    }

    public String getGender()
    {
        return gender;
    }

    public void setGender(String gender)
    {
        this.gender = gender;
    }

    public String getDesignation()
    {
        return designation;
    }

    public void setDesignation(String designation)
    {
        this.designation = designation;
    }

    public Integer getSalary()
    {
        return salary;
    }

    public void setSalary(Integer salary)
    {
        this.salary = salary;
    }

    public Integer getBonus()
    {
        return bonus;
    }

    public void setBonus(Integer bonus)
    {
        this.bonus = bonus;
    }

    public Integer getPerks()
    {
        return perks;
    }

    public void setPerks(Integer perks)
    {
        this.perks = perks;
    }

    public Employee(String name, String department, String gender, String designation, Integer salary, Integer bonus,
            Integer perks)
    {
        super();
        this.name = name;
        this.department = department;
        this.gender = gender;
        this.designation = designation;
        this.salary = salary;
        this.bonus = bonus;
        this.perks = perks;
    }



    public Employee()
    {
        super();
    }

    public static void main(String[] args) {
        List<Employee> values = new ArrayList<>();
        values.add(new Employee("bill", "dep1", "male", "des1", 100000, 5000, 20));
        values.add(new Employee("john", "dep1", "male", "des1", 80000, 4000, 10));
        values.add(new Employee("lisa", "dep1", "female", "des1", 80000, 4000, 10));
        values.add(new Employee("rosie", "dep1", "female", "des2", 70000, 3000, 15));
        values.add(new Employee("will", "dep2", "male", "des1", 60000, 3500, 18));
        values.add(new Employee("murray", "dep2", "male", "des1", 70000, 3000, 13));

        Map<EmployeeGroup, Employee> resultMap = values.stream().collect(Collectors.groupingBy(e-> new EmployeeGroup(e) , new EmployeeCollector()));

        System.out.println(new ArrayList(resultMap.values()));
    }

    @Override
    public String toString()
    {
        return "Employee [name=" + name + ", department=" + department + ", gender=" + gender + ", designation=" + designation + ", salary=" + salary + ", bonus=" + bonus + ", perks=" + perks + "]";
    }

}

Class for the aggregating key

public class EmployeeGroup
{

    private String department;

    private String gender;

    private String designation;

    public String getDepartment()
    {
        return department;
    }

    public void setDepartment(String department)
    {
        this.department = department;
    }

    public String getGender()
    {
        return gender;
    }

    public void setGender(String gender)
    {
        this.gender = gender;
    }

    public String getDesignation()
    {
        return designation;
    }

    public void setDesignation(String designation)
    {
        this.designation = designation;
    }

    public EmployeeGroup(Employee employee) {
        this.department = employee.getDepartment();
        this.gender = employee.getGender();
        this.designation = employee.getDesignation();
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((department == null) ? 0 : department.hashCode());
        result = prime * result + ((designation == null) ? 0 : designation.hashCode());
        result = prime * result + ((gender == null) ? 0 : gender.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        EmployeeGroup other = (EmployeeGroup) obj;
        if (department == null)
        {
            if (other.department != null)
                return false;
        } else if (!department.equals(other.department))
            return false;
        if (designation == null)
        {
            if (other.designation != null)
                return false;
        } else if (!designation.equals(other.designation))
            return false;
        if (gender == null)
        {
            if (other.gender != null)
                return false;
        } else if (!gender.equals(other.gender))
            return false;
        return true;
    }

}

Collector

public class EmployeeCollector implements Collector<Employee, Employee, Employee> {

    private Map<EmployeeGroup,Integer> countMap = new HashMap<>();

    @Override
    public Supplier<Employee> supplier() {
        return () -> new Employee();
    }

    @Override
    public BiConsumer<Employee, Employee> accumulator() {
        return this::accumulator;
    }

    @Override
    public BinaryOperator<Employee> combiner() {
        return this::accumulator;
    }

    @Override
    public Function<Employee, Employee> finisher() {
        return e -> {
            Integer count = countMap.get(new EmployeeGroup(e));
            e.setBonus(e.getBonus()/count);
            e.setPerks(e.getPerks()/count);
            e.setSalary(e.getSalary()/count);
            return e;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Stream.of(Characteristics.UNORDERED)
                .collect(Collectors.toCollection(HashSet::new));
    }

    public Employee accumulator(Employee subtotal, Employee element) {
        if (subtotal.getDepartment() == null) {
            subtotal.setDepartment(element.getDepartment());
            subtotal.setGender(element.getGender());
            subtotal.setDesignation(element.getDesignation());
            subtotal.setPerks(element.getPerks());
            subtotal.setSalary(element.getSalary());
            subtotal.setBonus(element.getBonus());
            countMap.put(new EmployeeGroup(subtotal), 1);
        } else {
            subtotal.setPerks(subtotal.getPerks() + element.getPerks());
            subtotal.setSalary(subtotal.getSalary() + element.getSalary());
            subtotal.setBonus(subtotal.getBonus() + element.getBonus());
            EmployeeGroup group = new EmployeeGroup(subtotal);
            countMap.put(group, countMap.get(group)+1);
        }
        return subtotal;
    }

}
0
On

To group by multiple values you could:

  • Create a class representing the grouped values, map each employee to a grouped instance and then group by it.

  • Use nested downstreams within the groupingby operation. However, as a result you would get nested maps to deal with.

  • Use a List initialized with the values you want to group by (easy and quick workaround).

Here's also a link with more in depth solutions on how to group by multiple values:

Group by multiple field names in java 8

Solution with List of Default Employees (what you requested)

//Grouping by multiple fields with a List workaround instead of using nested groupingBy downstreams that would return nested Maps
Map<List, List<Employee>> mapGroupedBy = listEmployees.stream()
        .collect(Collectors.groupingBy(e -> Arrays.asList(e.getDepartment(), e.getDesignation(), e.getGender())));

//Returning an ArrayList with the average values
List<Employee> listEmployeesResult = mapGroupedBy.values().stream()
        .collect(ArrayList::new,
                (listCollect, listGrouped) -> listCollect.add(new Employee(null, null, null, null,
                        (int) Math.round(listGrouped.stream().collect(Collectors.averagingDouble(Employee::getSalary)).doubleValue()),
                        (int) Math.round(listGrouped.stream().collect(Collectors.averagingDouble(Employee::getBonus)).doubleValue()),
                        (int) Math.round(listGrouped.stream().collect(Collectors.averagingDouble(Employee::getPerks)).doubleValue()))),
                (list1, list2) -> list1.addAll(list2));

The problem I see with what you requested is that you won't be able to tell from which group of values each averaged employee comes from.

Solution with Map of Default Employees (my recommendation)

//Grouping by multiple fields with a List workaround instead of using nested groupingBy downstreams that would return nested Maps
Map<List, List<Employee>> mapGroupedBy = listEmployees.stream()
        .collect(Collectors.groupingBy(e -> Arrays.asList(e.getDepartment(), e.getDesignation(), e.getGender())));

//Map of known keys for each n-upla (list) of values
Map<List, Employee> mapResult = new HashMap<>();

//For each entry of the grouped map we generate a new entry for the result map by "mapping" each grouped list into a default Employee with no information and average values
mapGroupedBy.entrySet().stream()
        .forEach(entry -> mapResult.put(entry.getKey(), new Employee(null, null, null, null,
                (int) Math.round(entry.getValue().stream().collect(Collectors.averagingDouble(Employee::getSalary)).doubleValue()),
                (int) Math.round(entry.getValue().stream().collect(Collectors.averagingDouble(Employee::getBonus)).doubleValue()),
                (int) Math.round(entry.getValue().stream().collect(Collectors.averagingDouble(Employee::getPerks)).doubleValue()))));

In this scenario, you're more likely to discern your output and work with it.

Test Main

public class Test {
    public static void main(String[] args) {
        List<Employee> listEmployees = new ArrayList<>(List.of(
                new Employee("Mark Hoppus", "Marketing", "male", "Sales Manager", 2200, 200, 1),
                new Employee("Tom DeLonge", "Marketing", "male", "Sales Manager", 2800, 0, 1),
                new Employee("Travis Barker", "Marketing", "male", "Sales Manager", 3850, 800, 6),
                new Employee("Aretha Franklin", "Marketing", "female", "Sales Manager", 2900, 300, 3),
                new Employee("Diana Ross", "Marketing", "female", "Sales Manager", 1900, 0, 1),
                new Employee("Keith Flint", "R&D", "male", "Software Engineer", 4000, 600, 0),
                new Employee("Liam Howlett", "R&D", "male", "Software Engineer", 5200, 250, 2),
                new Employee("Whitney Houston", "R&D", "female", "Software Engineer", 6000, 1000, 8),
                new Employee("Tina Turner", "R&D", "female", "Software Engineer", 7500, 450, 9)
        ));

        //Grouping by multiple fields with a List workaround instead of using nested groupingBy downstreams that would return nested Maps
        Map<List, List<Employee>> mapGroupedBy = listEmployees.stream()
                .collect(Collectors.groupingBy(e -> Arrays.asList(e.getDepartment(), e.getDesignation(), e.getGender())));

        //Returning an ArrayList with the average values
        List<Employee> listEmployeesResult = mapGroupedBy.values().stream()
                .collect(ArrayList::new,
                        (listCollect, listGrouped) -> listCollect.add(new Employee(null, null, null, null,
                                (int) Math.round(listGrouped.stream().collect(Collectors.averagingDouble(Employee::getSalary)).doubleValue()),
                                (int) Math.round(listGrouped.stream().collect(Collectors.averagingDouble(Employee::getBonus)).doubleValue()),
                                (int) Math.round(listGrouped.stream().collect(Collectors.averagingDouble(Employee::getPerks)).doubleValue()))),
                        (list1, list2) -> list1.addAll(list2));

        //Printing the ArrayList with no indication of where those average values come from
        System.out.println("Printing list results");
        for (Employee e : listEmployeesResult) {
            System.out.printf("Salary: %d - Bonus: %d - Perks: %d%n", e.getSalary(), e.getBonus(), e.getPerks());
        }

        //Map of known keys for each n-upla (list) of values
        Map<List, Employee> mapResult = new HashMap<>();

        //For each entry of the grouped map we generate a new entry for the result map by "mapping" each grouped list into a default Employee with no information and average values
        mapGroupedBy.entrySet().stream()
                .forEach(entry -> mapResult.put(entry.getKey(), new Employee(null, null, null, null,
                        (int) Math.round(entry.getValue().stream().collect(Collectors.averagingDouble(Employee::getSalary)).doubleValue()),
                        (int) Math.round(entry.getValue().stream().collect(Collectors.averagingDouble(Employee::getBonus)).doubleValue()),
                        (int) Math.round(entry.getValue().stream().collect(Collectors.averagingDouble(Employee::getPerks)).doubleValue()))));

        System.out.println("\nPrinting map results");
        for (List keyList : mapResult.keySet()) {
            System.out.printf("%s => Salary: %d - Bonus: %d - Perks: %d%n", keyList, mapResult.get(keyList).getSalary(), mapResult.get(keyList).getBonus(), mapResult.get(keyList).getPerks());
        }
    }
}

Here, I've implemented both solutions and showed their differences.

Output

For some reasons, the output is not displayed and I had to paste the link.

https://i.stack.imgur.com/fHDun.png