如何在Java Stream中使用MaxBy进行分组和聚合

huangapple go评论78阅读模式
英文:

How to group and Aggregate using MaxBy in Java Stream

问题

以下是翻译好的内容:

我有一个如下所示的 Employee 对象列表。

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

需求是获取每个部门中薪水最高的员工姓名列表。在同一个部门中可能有多个薪水相同的员工。例如,销售团队可能有2名员工,比如 Alex 和 John,他们的薪水都是最高的。

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

期望的输出是 -> Map(部门, 最高薪水的员工姓名列表)

使用下面的代码,我只能得到一个员工对象作为输出,即使在同一个部门中可能有多个满足条件的员工。

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

这是因为 MaxBy 方法在找到满足给定条件的第一个项时就停止了。

我的问题是:

  1. 如何在每个部门中获取薪水最高的员工列表(Map<String, List<Employee>>)?
  2. 如何仅检索每个部门中薪水最高的员工姓名列表(Map<String, List<String>>)?
英文:

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&lt;Employee&gt; eList = new ArrayList&lt;&gt;();
eList .add(new Employee(101, &quot;ALEX&quot;,&quot;SALES&quot;,7000));
eList.add(new Employee(102, &quot;KATHY&quot;,&quot;ADMIN&quot;,3000));
eList.add(new Employee(103, &quot;JOHN&quot;,&quot;SALES&quot;,7000));
eList.add(new Employee(104, &quot;KONG&quot;,&quot;IT&quot;,5000));
eList.add(new Employee(105, &quot;MIKE&quot;,&quot;SALES&quot;,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&lt;String, Employee&gt; 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 &lt;String, List&lt;Employee&gt;&gt;)in each department?
  2. How to retrieve the list of highest-paid employee names alone (Map &lt;String, List&lt;String&gt;&gt;) in each department?

答案1

得分: 1

首先,使用Collectors.toMap创建一个基于部门的最高工资映射:

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

然后,通过基于部门的最高工资筛选员工列表,然后按部门分组。这样你就能得到每个部门工资最高的员工。

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

如果只想要员工姓名,你可以在groupingBy中使用Collectors.mapping

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())));
英文:

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

Map&lt;String, Integer&gt; 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&lt;String, List&lt;Employee&gt;&gt; resEmpMap =
    eList.stream()
         .filter(e -&gt; 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&lt;String, List&lt;String&gt;&gt; resEmpMap =
      eList.stream()
           .filter(e -&gt; maxSalByDept.get(e.getDept()).equals(e.getSalary()))
           .collect(Collectors.groupingBy(Employee::getDept,
                Collectors.mapping(Employee::getName, Collectors.toList())));

答案2

得分: 0

如果你编写自己的收集器这其实很简单我在没有进行真正搜索之前就写了这个但实际上库[StreamEx](https://github.com/amaembo/streamex)实际上有[一个内置的用于此目的的收集器](https://amaembo.github.io/streamex/javadoc/one/util/streamex/MoreCollectors.html#maxAll(java.util.Comparator))。

private static <T> Collector<T, List<T>, List<T>> allMax(Comparator<T> comparator) {
    return Collector.of(
        ArrayList::new,
        (list, item) -> {
            if (list.isEmpty()) { // 如果这是第一个元素,保证它是最大的
                list.add(item);
                return;
            }

            int comparison = comparator.compare(item, list.get(0));
            if (comparison > 0) { // 如果该项大于当前最大值,舍弃它
                list.clear();
                list.add(item);
            }
            else if (comparison == 0) { // 如果该项等于当前最大值,添加它
                list.add(item);
            }
        },
        // 仅在并行流中需要,如果需要可以实现,但不是必需的
        (listA, listB) -> { throw new UnsupportedOperationException(); } 
    );
}

然后你的代码变成了

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

要仅获取名字你的第二个问题),你可以使用`collectingAndThen`将员工列表映射为仅包含他们名字的列表

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())
            )
        )
    );
英文:

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 &lt;T&gt; Collector&lt;T, List&lt;T&gt;, List&lt;T&gt;&gt; allMax(Comparator&lt;T&gt; comparator) {
return Collector.of(
ArrayList::new,
(list, item) -&gt; {
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 &gt; 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) -&gt; { throw new UnsupportedOperationException(); } 
);
}

Your code then becomes

Map&lt;String, List&lt;Employee&gt;&gt; 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&lt;String, List&lt;String&gt;&gt; collect = eList.stream()
.collect(
Collectors.groupingBy(
Employee::getDept,
Collectors.collectingAndThen(
allMax(Comparator.comparing(Employee::getSalary)),
(emps) -&gt; emps.stream().map(Employee::getName).collect(Collectors.toList())
)
)
);

答案3

得分: 0

以下是翻译好的部分:

提供的数据:

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

以下是获取每个部门的最高薪水的代码:

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

打印结果:

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

以下是获取只有姓名的代码:

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

打印结果:

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

你的类已经被我修改,添加了构造函数、getter方法和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);
    }
}
英文:

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&lt;Employee&gt; eList = new ArrayList&lt;&gt;();
eList.add(new Employee(101, &quot;ALEX&quot;, &quot;SALES&quot;, 7000));
eList.add(new Employee(102, &quot;KATHY&quot;, &quot;ADMIN&quot;, 3000));
eList.add(new Employee(103, &quot;JOHN&quot;, &quot;SALES&quot;, 7000));
eList.add(new Employee(104, &quot;KONG&quot;, &quot;IT&quot;, 5000));
eList.add(new Employee(106, &quot;MIKE&quot;, &quot;SALES&quot;, 4000));
eList.add(new Employee(107, &quot;BOB&quot;, &quot;IT&quot;, 4000));
eList.add(new Employee(108, &quot;SARAH&quot;, &quot;ADMIN&quot;, 2000));
eList.add(new Employee(109, &quot;RALPH&quot;, &quot;SALES&quot;, 4000));
eList.add(new Employee(110, &quot;JUNE&quot;, &quot;ADMIN&quot;, 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&lt;String, List&lt;Employee&gt;&gt; highestSalaries = eList.stream()
.collect(Collectors.groupingBy(Employee::getDept,
Collectors.groupingBy(Employee::getSalary,
TreeMap::new, Collectors.toList())))
.entrySet().stream()
.collect(Collectors.toMap(e -&gt; e.getKey(),
e -&gt; 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&lt;String, List&lt;String&gt;&gt; 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(&quot;{%s,  %s,  %s,  %s}&quot;, eno, name,
dept, salary);
}
}
}
</details>

huangapple
  • 本文由 发表于 2020年9月8日 03:08:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/63783027.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定