Java8 Stream Collectors.GroupingBy用法详解示例

后端 潘老师 6个月前 (10-30) 165 ℃ (0) 扫码查看

学习如何使用 Collectors.groupingBy() 方法对Stream元素进行分组和聚合,类似于SQL中的’GROUP BY’子句。

1.Collectors.groupingBy() 方法

1.1 语法

groupingBy() 方法返回一个实现了Stream元素“GROUP BY”操作的 Collector,并将结果返回为 Map。

groupingBy(classifier)
groupingBy(classifier, collector)
groupingBy(classifier, supplier, collector)

我们可以向此方法传递以下参数:

  • classifier:将输入元素映射为 Map 键
  • collector:是下游的归约函数。默认情况下,使用 Collectors.toList(),将分组后的元素放入一个 List 中。
  • supplier:提供一个空的 Map,结果将被插入其中。默认情况下,使用 HashMap::new。可以使用其他 Map 类型,如 TreeMap、LinkedHashMap 或 ConcurrentMap,以在分组过程中插入其他行为,例如排序。

1.2 使用 groupingByConcurrent() 进行并行处理

如果我们希望并行处理 Stream 元素,可以使用 Collectors.groupingByConcurrent(),它使用CPU的多核心架构并返回一个 ConcurrentMap。除了并发性之外,它的工作方式与 groupingBy() 方法类似。

groupingByConcurrent(classifier)
groupingByConcurrent(classifier, collector)
groupingByConcurrent(classifier, supplier, collector)

2.Collectors groupingBy 示例

为了演示的目的,我们创建两个记录类 Person 和 Department 如下:

record Person(int id, String name, double salary, Department department) {}
record Department(int id, String name) {}
提示:Record类类似Lombok属性只读对象,只有get方法,没有set方法

我们创建一个包含人员的 List,我们将使用它来创建 Stream 并以不同的分组收集记录。

List<Person> persons = List.of(
    new Person(1, "Alex", 100d, new Department(1, "HR")),
    new Person(2, "Brian", 200d, new Department(1, "HR")),
    new Person(3, "Charles", 900d, new Department(2, "Finance")),
    new Person(4, "David", 200d, new Department(2, "Finance")),
    new Person(5, "Edward", 200d, new Department(2, "Finance")),
    new Person(6, "Frank", 800d, new Department(3, "ADMIN")),
    new Person(7, "George", 900d, new Department(3, "ADMIN")));

2.1 按简单条件分组

让我们从一个简单的条件开始,以更好地理解用法。在以下示例中,我们按部门对所有人进行分组。这将创建一个 Map<Department, List<Person>>,其中 map 键是部门记录,map 值将是该部门中的所有人员。

Map<Department, List<Person>> map = persons.stream().collect(groupingBy(Person::department));
System.out.println(map);

程序的输出如下:

{
    Department[id=2, name=Finance]=[
    Person[id=3, ...],
    Person[id=4, ...],
    Person[id=5, ...],
    Department[id=3, name=ADMIN]=[
    Person[id=6, ...],
    Person[id=7, ...],
    Department[id=1, name=HR]=[
    Person[id=1, ...],
    Person[id=2, ...]
}

类似地,如果我们希望收集所有部门中的人员ID,而不是收集人员记录,可以使用 Collectors.mapping() 来收集。

Map<Department, List<Integer>> map = persons.stream()
        .collect(groupingBy(Person::department, mapping(Person::id, toList())));
System.out.println(map);

输出:

{
    Department[id=2, name=Finance]=[3, 4, 5],
    Department[id=3, name=ADMIN]=[6, 7],
    Department[id=1, name=HR]=[1, 2]
}

2.2. 根据复杂条件进行分组

有时候,我们可能需要应用复杂的条件来进行分组。在这种情况下,Map可以使用Java元组来表示条件,然后将匹配的元素组合成一个列表,作为Map的值。

在下面的例子中,我们想要根据不同的部门和薪水对进行分组。在Map的值中,我们会得到一个具有相同部门和相同薪水的人的列表。

Map<Object, List<Integer>> map = persons.stream()
    .collect(groupingBy(person -> new Pair<>(person.salary(), person.department()),
        mapping(Person::id, toList())));
System.out.println(map);

程序输出清楚地表明,第4人和第5人在相同的部门并且拥有相同的薪水。

{
    [900.0, Department[id=3, name=ADMIN]]=[7],
    [800.0, Department[id=3, name=ADMIN]]=[6],
    [200.0, Department[id=2, name=Finance]]=[4, 5],
    [900.0, Department[id=2, name=Finance]]=[3],
    [200.0, Department[id=1, name=HR]]=[2],
    [100.0, Department[id=1, name=HR]]=[1]
}

2.3. 分组并计数

我们还可以执行其他操作来聚合值,例如计数、平均值、求和等。这有助于对Map的值进行归约操作以产生单个值。

在下面的例子中,我们正在计算一个部门中的所有人数。

Map<Department, Long> map = persons.stream()
    .collect(groupingBy(Person::department, counting()));
System.out.println(map);

程序输出显示了每个部门的人数。

{
    Department[id=2, name=Finance]=3,
    Department[id=3, name=ADMIN]=2,
    Department[id=1, name=HR]=2
}

同样,我们可以计算具有相同薪水的人数。

Map<Double, Long> map = persons.stream()
    .collect(groupingBy(Person::salary, counting()));
System.out.println(map);

程序输出显示了每个薪水级别的员工数量。

{800.0=1, 200.0=3, 100.0=1, 900.0=2}

2.4. 分组并求平均值

可以执行其他聚合操作,例如找到每个部门的平均薪水。

Map<Department, Double> map = persons.stream()
    .collect(groupingBy(Person::department, averagingDouble(Person::salary)));
System.out.println(map);

程序输出:

{
    Department[id=2, name=Finance]=433.3333333333333,
    Department[id=3, name=ADMIN]=850.0,
    Department[id=1, name=HR]=150.0
}

2.5. 分组并找到最大/最小值

要找到分组值的最大或最小值,请使用maxBy()或minBy()收集器,并将Comparator参数传递给比较所需字段的值。

在下面的例子中,我们正在查找每个部门的最高薪水的人。

Map<Department, Optional<Person>> map = persons.stream()
    .collect(groupingBy(Person::department, maxBy(Comparator.comparingDouble(Person::salary))));
System.out.println(map);

程序输出:

{
    Department[id=2, name=Finance]=Optional[Person[id=3, name=Charles, salary=900.0,...]],
    Department[id=3, name=ADMIN]=Optional[Person[id=7, name=George, salary=900.0, ...]],
    Department[id=1, name=HR]=Optional[Person[id=2, name=Brian, salary=200.0, ...]]
}

2.6. 分组并进行过滤

Stream.filter()方法会在传递给下一个操作之前过滤掉所有不匹配的元素。这可能不是理想的解决方案。

考虑以下示例,我们将按部门对所有薪水大于300的员工进行分组。

Map<Department, Long> map = persons.stream()
    .filter(p -> p.salary() > 300d)
    .collect(groupingBy(Person::department, counting()));
System.out.println(map);

程序输出:

{
    Department[id=2, name=Finance]=1,
    Department[id=3, name=ADMIN]=2
}

上述程序输出完全省略了department-1,因为该部门没有符合条件的人员。但是,如果我们想列出所有没有匹配值的Map键,那么我们可以在将值添加到Map时使用Collectors.filtering()方法进行过滤。

Map<Department, Long> map = persons.stream()
    .collect(groupingBy(Person::department, filtering(p -> p.salary() > 300d, counting())));

System.out.println(map);

程序输出:

{
    Department[id=2, name=Finance]=1,
    Department[id=3, name=ADMIN]=2,
    Department[id=1, name=HR]=0
}

现在,department-1也被列为匹配记录为零。

3. 结论

Collectors的groupBy()方法非常适合根据各种条件对Stream元素进行分组,并在Map值上执行各种聚合操作。如上述示例所示,我们可以使用Collectors的组合来执行任何类型的分组。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/10501.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】