简体   繁体   English

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

[英]Aggregate multiple fields grouping by multiple fields in Java 8

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 .在下面的Employee designation中,我想获得按department 、职位和gender分组的所有员工的平均salary 、平均bonus和平均perks ,并希望结果是一个List<Employee> ,其中包含salarybonusperks

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?这样做的干净方法是什么?

You can do this by creating a class for the grouping key and writing a collector:您可以通过为分组键创建一个 class 并编写一个收集器来完成此操作:

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.我只是简单地添加每个键的值并计算 map 中的出现次数。在完成器中,我通过计数来划分总和。

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...您可以通过对 Employee 进行分类,添加计数并将此 class 用于供应商/小计并使用一些转换来摆脱 countMap ...

You could also make to groupBys one for the sum and another for the count and computing the avarages with the two created maps...您还可以将 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 for the aggregating key聚合键为 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;
    }

}

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

}

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.创建一个 class 表示分组值,map 每个员工到一个分组实例,然后按它分组。

  • Use nested downstreams within the groupingby operation.groupingby操作中使用嵌套下游。 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).使用用您想要分组的值初始化的List (简单快速的解决方法)。

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 按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)默认员工 Map 的解决方案(我的推荐)

//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.在这种情况下,您更有可能辨别出您的 output 并使用它。

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 Output

For some reasons, the output is not displayed and I had to paste the link.由于某些原因,output 没有显示,我不得不粘贴链接。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM