如何在应用嵌套分组时保留所有子组通过收集器

发布于 2025-01-13 13:54:57 字数 616 浏览 3 评论 0 原文

我正在尝试按性别部门员工列表进行分组。

如何确保所有部门都包含在每个性别排序顺序中,即使相关性别计数为零嗯>?

目前,我有以下代码和输出

employeeRepository.findAll().stream()
            .collect(Collectors.groupingBy(Employee::getGender, 
                        Collectors.groupingBy(Employee::getDepartment, 
                                              Collectors.counting())));

//output
//{MALE={HR=1, IT=1}, FEMALE={MGMT=1}}

首选输出是:

{MALE={HR=1, IT=1, MGMT=0}, FEMALE={HR=0, IT=0, MGMT=1}}

I am trying to group a list of employees by the gender and department.

How do I ensure all departments are included in a sorted order for each gender, even when the relevant gender count is zero?

Currently, I have the following code and output

employeeRepository.findAll().stream()
            .collect(Collectors.groupingBy(Employee::getGender, 
                        Collectors.groupingBy(Employee::getDepartment, 
                                              Collectors.counting())));

//output
//{MALE={HR=1, IT=1}, FEMALE={MGMT=1}}

Preferred output is:

{MALE={HR=1, IT=1, MGMT=0}, FEMALE={HR=0, IT=0, MGMT=1}}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

油饼 2025-01-20 13:54:57

为此,首先必须按部门分组,然后才按性别分组,而不是相反。

第一个收集器 groupingBy(Employee::getDepartment, _downstream_ ) 将根据部门将数据集拆分为组。由于下游收集器 partitioningBy(employee -> employee.getGender() == Employee.Gender.MALE, _downstream_ ) 将被应用,它将映射到每个部门的数据划分为 根据员工性别分为两部分。最后,作为下游应用的 Collectors.counting() 将提供每个部门<的每个性别员工的总数 /强>。

因此,由 collect() 操作生成的中间 map 类型将为 Map> - < em>每个部门性别划分的员工人数(布尔值)(为了简单起见,部门是一个纯字符串< /em>)。

下一步将此映射转换为 Map> - 每个部门员工人数 性别

我的方法是在条目集上创建一个流,并将每个条目替换为一个新条目,该新条目将性别作为其,并且为了保留有关部门的信息,其依次将是一个以部门为键和一个带有部门的条目>按部门计算作为其值。

然后通过条目键使用groupingBy收集条目流。应用映射作为下游收集器来提取嵌套条目。然后应用Collectors.toMap()Map.Entry类型的条目收集到地图中。

所有部门都包含在排序顺序中

为了确保嵌套地图中的顺序(按计数划分NavigableMap 应该使用。

为此,需要使用需要 mapFactorytoMap() 风格(它还需要一个 mergeFunction这对于此任务来说并不是真正有用,因为不会有重复项,但也必须提供它)。

public static void main(String[] args) {
    List<Employee> employeeRepository = 
            List.of(new Employee("IT", Employee.Gender.MALE),
                    new Employee("HR", Employee.Gender.MALE),
                    new Employee("MGMT", Employee.Gender.FEMALE));

    Map<Employee.Gender, NavigableMap<String, Long>> departmentCountByGender = employeeRepository
            .stream()
            .collect(Collectors.groupingBy(Employee::getDepartment, // Map<String, Map<Boolean, Long>> - department to *employee count* by gender
                        Collectors.partitioningBy(employee -> employee.getGender() == Employee.Gender.MALE,
                                                  Collectors.counting())))
            .entrySet().stream()
            .flatMap(entryDep -> entryDep.getValue().entrySet().stream()
                    .map(entryGen -> Map.entry(entryGen.getKey() ? Employee.Gender.MALE : Employee.Gender.FEMALE,
                                               Map.entry(entryDep.getKey(), entryGen.getValue()))))
            .collect(Collectors.groupingBy(Map.Entry::getKey,
                        Collectors.mapping(Map.Entry::getValue,
                                Collectors.toMap(Map.Entry::getKey,
                                                 Map.Entry::getValue,
                                                 (v1, v2) -> v1,
                                                 TreeMap::new))));

    System.out.println(departmentCountByGender);
}

用于演示目的的虚拟 Employee class

class Employee {
    enum Gender {FEMALE, MALE};

    private String department;
    private Gender gender;
    // etc.
    
    // constructor, getters
}

输出

{FEMALE={HR=0, IT=0, MGMT=1}, MALE={HR=1, IT=1, MGMT=0}}

To achieve that, first you have to group by department, and only then by gender, not the opposite.

The first collector groupingBy(Employee::getDepartment, _downstream_ ) will split the data set into groups based on department. As it downstream collector partitioningBy(employee -> employee.getGender() == Employee.Gender.MALE, _downstream_ ) will be applied, it'll divide the data mapped to each department into two parts based on the employee gender. And finally, Collectors.counting() applied as a downstream will provide the total number of employees of each gender for every department.

So the intermediate map produced by the collect() operation will be of type Map<String, Map<Boolean, Long>> - employee count by gender (Boolean) for each department (for simplicity, department is a plain string).

The next step in transform this map into Map<Employee.Gender, Map<String, Long>> - employee count by department for each gender.

My approach is to create a stream over the entry set and replace each entry with a new one, which will hold a gender as its key and in order to preserve the information about a department its value in turn will be an entry with a department as a key and a with a count by department as its value.

Then collect the stream of entries with groupingBy by the entry key. Apply mapping as a downstream collector to extract the nested entry. And then apply Collectors.toMap() to collect entries of type Map.Entry<String, Long> into map.

all departments are included in a sorted order

To insure the order in the nested map (department by count) a NavigableMap should be used.

In order to do that, a flavor of toMap() that expects a mapFactory needs to be used (it also expects a mergeFunction which isn't really useful for this task since there will be no duplicates, but it has to be provided as well).

public static void main(String[] args) {
    List<Employee> employeeRepository = 
            List.of(new Employee("IT", Employee.Gender.MALE),
                    new Employee("HR", Employee.Gender.MALE),
                    new Employee("MGMT", Employee.Gender.FEMALE));

    Map<Employee.Gender, NavigableMap<String, Long>> departmentCountByGender = employeeRepository
            .stream()
            .collect(Collectors.groupingBy(Employee::getDepartment, // Map<String, Map<Boolean, Long>> - department to *employee count* by gender
                        Collectors.partitioningBy(employee -> employee.getGender() == Employee.Gender.MALE,
                                                  Collectors.counting())))
            .entrySet().stream()
            .flatMap(entryDep -> entryDep.getValue().entrySet().stream()
                    .map(entryGen -> Map.entry(entryGen.getKey() ? Employee.Gender.MALE : Employee.Gender.FEMALE,
                                               Map.entry(entryDep.getKey(), entryGen.getValue()))))
            .collect(Collectors.groupingBy(Map.Entry::getKey,
                        Collectors.mapping(Map.Entry::getValue,
                                Collectors.toMap(Map.Entry::getKey,
                                                 Map.Entry::getValue,
                                                 (v1, v2) -> v1,
                                                 TreeMap::new))));

    System.out.println(departmentCountByGender);
}

Dummy Employee class used for demo-purposes:

class Employee {
    enum Gender {FEMALE, MALE};

    private String department;
    private Gender gender;
    // etc.
    
    // constructor, getters
}

Output

{FEMALE={HR=0, IT=0, MGMT=1}, MALE={HR=1, IT=1, MGMT=0}}
执妄 2025-01-20 13:54:57

您可以继续处理代码的结果:

List<String> deptList = employees.stream().map(Employee::getDepartment).sorted().toList();

Map<Gender, Map<String, Long>> tmpResult = employees.stream()
        .collect(Collectors.groupingBy(Employee::getGender, Collectors.groupingBy(Employee::getDepartment, Collectors.counting())));

Map<Gender, Map<String, Long>> finalResult = new HashMap<>();

for (Map.Entry<Gender, Map<String, Long>> entry : tmpResult.entrySet()) {
    Map<String, Long> val = new LinkedHashMap<>();
    for (String dept : deptList) {
        val.put(dept, entry.getValue().getOrDefault(dept, 0L));
    }

    finalResult.put(entry.getKey(), val);
}

System.out.print(finalResult);

如果您想用一行代码获得结果,则代码的可读性或可维护性可能不会很好。

但是,如果您不介意使用第三方库,还有一种选择: abacus-common

Map<Gender, Map<String, Integer>> result = Stream.of(employees)
        .groupByToEntry(Employee::getGender, MoreCollectors.countingIntBy(Employee::getDepartment)) // step 1) group by gender 
        .mapValue(it -> Maps.newMap(deptList, Fn.identity(), dept -> it.getOrDefault(dept, 0), IntFunctions.ofLinkedHashMap())) // step 2) process the value.
        .toMap();

声明:我是 abacus-common 的开发者

You can continue to work on the result of your code:

List<String> deptList = employees.stream().map(Employee::getDepartment).sorted().toList();

Map<Gender, Map<String, Long>> tmpResult = employees.stream()
        .collect(Collectors.groupingBy(Employee::getGender, Collectors.groupingBy(Employee::getDepartment, Collectors.counting())));

Map<Gender, Map<String, Long>> finalResult = new HashMap<>();

for (Map.Entry<Gender, Map<String, Long>> entry : tmpResult.entrySet()) {
    Map<String, Long> val = new LinkedHashMap<>();
    for (String dept : deptList) {
        val.put(dept, entry.getValue().getOrDefault(dept, 0L));
    }

    finalResult.put(entry.getKey(), val);
}

System.out.print(finalResult);

Probably readability or maintainability of code won't be good if you want to achieve result with one line of code.

However, there is one alternative if you don't mind to use third-party library: abacus-common

Map<Gender, Map<String, Integer>> result = Stream.of(employees)
        .groupByToEntry(Employee::getGender, MoreCollectors.countingIntBy(Employee::getDepartment)) // step 1) group by gender 
        .mapValue(it -> Maps.newMap(deptList, Fn.identity(), dept -> it.getOrDefault(dept, 0), IntFunctions.ofLinkedHashMap())) // step 2) process the value.
        .toMap();

Declaration: I'm the developer of abacus-common

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文