按 Java 8 中的多个字段聚合多个字段分组

Aggregate multiple fields grouping by multiple fields in Java 8

在下面的 Employee class 中,我想获得所有员工的平均值 salary、平均值 bonus 和平均值 perks departmentdesignationgender 并希望结果是 List<Employee> 以及 salarybonus 和 [=15= 的聚合值].

public class Employee {

    private String name;

    privte String department;

    private String gender;

    private String designation;

    private Integer salary;

    private Integer bonus;

    private Integer perks;
    
}

什么是干净的方法?

您可以通过为分组键创建一个 class 并编写一个收集器来做到这一点:

我只是将每个键的值相加并计算地图中的出现次数。在整理器中,我通过计数来划分总和。

您可以通过子类化 Employee、添加计数并将此 class 用于 supplier/subtotal 并使用一些强制转换来摆脱 countMap...

您还可以对 groupBys 进行一个求和,另一个用于计数并使用两个创建的映射计算平均数...

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 用于聚合键

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;
    }

}

收藏家

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;
    }

}

要按多个值分组,您可以:

  • 创建一个 class 表示分组值,将每个员工映射到一个分组实例,然后按它分组。

  • groupingby 操作中使用嵌套下游。但是,结果是您需要处理嵌套地图。

  • 使用 List 使用您要分组的值进行初始化(简单快捷的解决方法)。

这里还有一个 link 关于如何按多个值分组的更深入的解决方案:

Group by multiple field names in java 8

带有默认员工列表的解决方案(您所要求的)

//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));

我看到你所要求的问题是你无法分辨每个普通员工来自哪一组价值观。

默认员工地图解决方案(我的推荐)

//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()))));

在这种情况下,您更有可能辨别输出并使用它。

测试主要

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());
        }
    }
}

在这里,我实现了这两种解决方案并展示了它们的区别。

输出

由于某些原因,输出没有显示,我不得不粘贴 link。

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