Stream.Collectors groupingBy 与 partitioningBy
概述:当我们遇到一个集合,需要根据某个字段进行
分组
或者根据某种条件进行
分区
的时候,我们可以使用到
groupingBy
与
partitioningBy
。
GroupingBy
针对Stream中的元素去进行
group by
操作,来对不同组的数据进行收集。
使用
@Data
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
初始化数据
List<Person> people = Arrays.asList(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 25),
new Person("David", 35),
new Person("Aavid", 30)
);
根据对象中的字段进行分组
根据age进行分组
public static void main(String[] args) {
Map<Integer, List<Person>> groupingBy = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println(groupingBy);
}
### 输出
{35=[Person(name=David, age=35)], 25=[Person(name=Alice, age=25), Person(name=Charlie, age=25)], 30=[Person(name=Bob, age=30), Person(name=Aavid, age=30)]}
这样因为是直接根据age字段进行分组的所以生成了一个key为Integer类型,value为List的一个Map,因为分组后可能有多个所以这里的单参数方法就直接为我们生成了一个List以表示那些对象为一组,当然我们也可以替换这个收集器
。
分组后映射
有时我们并不希望我们是返回原Person对象,而是返回这个对象中的某个字段,或者根据某个计算后得到的一个值。这里类似与Stream.map
。
根据age进行分组,后得到每个对象的name
public static void main(String[] args) {
// 分组后映射
Map<Integer, List<String>> groupMap = people.stream()
.collect(Collectors.groupingBy(Person::getAge
, Collectors.mapping(Person::getName, Collectors.toList())));
System.out.println(groupMap);
}
### 输出
{35=[David], 25=[Alice, Charlie], 30=[Bob, Aavid]}
Collectors.mapping
的两个参数
- Function<? super T, ? extends U> mapper 根据原有对象生成一个新的对象
如何映射对象
- Collector<? super U, A, R> downstream 如何收集元素 可收集为
List,Set
等,当然我们也可以在此基础上继续一些其他收集操作,与collect
的方法一致
可以理解为Collectors.groupingBy
第二个参数是把分组后的集合当做Stream
再去操作了一下。所以这里可以map,filter,max
等。
分组后过滤
根据age进行分组,过滤age小于每组中小于等于25的元素
public static void main(String[] args) {
// 分组后过滤
Map<Integer, List<String>> groupingBy = people.stream()
.collect(Collectors.groupingBy(Person::getAge
, Collectors.filtering(person -> person.getAge() > 25, Collectors.toList())));
System.out.println(groupingBy);
}
### 输出
{35=[Person(name=David, age=35)], 25=[], 30=[Person(name=Bob, age=30), Person(name=Aavid, age=30)]}
我们可以看见25的这个组虽然还在但是组内已经没有元素了。这时你想想另外一种情况,先使用filter再去进行分组
public static void main(String[] args) {
// 分组后过滤
Map<Integer, List<String>> groupingBy = people.stream().filter(person -> person.getAge() > 25).collect(Collectors.groupingBy(Person::getAge));
System.out.println(groupingBy);
}
### 输出
{35=[Person(name=David, age=35)], 30=[Person(name=Bob, age=30), Person(name=Aavid, age=30)]}
我们发送25这个组直接就没了。这里要把这两个区分开来。
一个是分组后过滤组内元素。
一个是过滤集合元素后分组。
分组后取最大
根据age分组,取name最大的值
public static void main(String[] args) {
// 分组后取最大
Map<Integer, Optional<Person>> groupMax = people.stream()
.collect(Collectors.groupingBy(Person::getAge
, Collectors.maxBy(Comparator.comparing(Person::getName))));
System.out.println(groupMax);
}
### 输出
{35=Optional[Person(name=David, age=35)], 25=Optional[Person(name=Charlie, age=25)], 30=Optional[Person(name=Bob, age=30)]}
类似Stream.max
这里的返回值为什么是Optional 呢?
是因为有可能组内为空,例如上面的分组后过滤,所以这里采用是Optional
的形式返回。确保value不为null对象。
自定义分组
根据name的首字母进行分组。
public static void main(String[] args) {
// 自定义分组
Map<Character, List<Person>> groupConsume = people.stream()
.collect(Collectors.groupingBy(person -> person.getName().charAt(0)));
System.out.println(groupConsume);
}
### 输出
{A=[Person(name=Alice, age=25), Person(name=Aavid, age=30)], B=[Person(name=Bob, age=30)], C=[Person(name=Charlie, age=25)], D=[Person(name=David, age=35)]}
只要我们提供的key一致则会被分配到同一个组中。
分组后总和
根据age是否大于25分组
public static void main(String[] args) {
// summingInt 返回 总和值
Map<Boolean, Integer> groupSum = people.stream()
.collect(Collectors.groupingBy(person -> person.getAge() > 25
, Collectors.summingInt(Person::getAge)));
System.out.println(collect);
}
### 输出
{false=50, true=95}
类似Stream.sum
分组后统计
根据age是否大于25分组
public static void main(String[] args) {
// summarizingInt 返回 IntSummaryStatistics 包含了 最大值,最小值,数量 平均数 总和等统计信息
Map<Boolean, IntSummaryStatistics> groupStatistics = people.stream()
.collect(Collectors.groupingBy(person -> person.getAge() > 25
, Collectors.summarizingInt(Person::getAge))));
System.out.println(groupStatistics);
}
### 输出
{false=IntSummaryStatistics{count=2, sum=50, min=25, average=25.000000, max=25}, true=IntSummaryStatistics{count=3, sum=95, min=30, average=31.666667, max=35}}
注意这里的返回值是Map<Boolean,IntSummaryStatistics>
IntSummaryStatistics中有
- count 个数
- sum 总和
- min 最小
- max 最大
- average 平均值
比较适合统计简答数据的一些业务。
多重分组
根据age分组后再根据首字母分组
public static void main(String[] args) {
// 多级分组 嵌套map
Map<Integer, Map<Character, List<Person>>> multiGroupingBy = people.stream()
.collect(Collectors.groupingBy(Person::getAge
, Collectors.groupingBy(person -> person.getName().charAt(0))));
System.out.println(multiGroupingBy);
}
### 输出
{35={D=[Person(name=David, age=35)]}, 25={A=[Person(name=Alice, age=25)], C=[Person(name=Charlie, age=25)]}, 30={A=[Person(name=Aavid, age=30)], B=[Person(name=Bob, age=30)]}}
注意这里的返回值是嵌套map
PartitioningBy
这里并不是指
partition by
窗口函数,而是根据一个Predicate(断言)
来区分为true,false的各部分数据。类似与Collectors.groupingBy(person -> person.getAge() > 25)
这里其实也只有两个组一个是true,一个是false
使用
断言分区后的数据大体可以跟groupingBy的使用方式一直。这里只简单使用演示一下。
简单使用
public static void main(String[] args) {
Map<Boolean, List<Person>> partitionBy = people.stream().collect(Collectors.partitioningBy(person -> person.getAge() > 25));
System.out.println(partitionBy);
}
### 输出
{false=[Person(name=Alice, age=25), Person(name=Charlie, age=25)], true=[Person(name=Bob, age=30), Person(name=David, age=35), Person(name=Aavid, age=30)]}
与 Collectors.groupingBy(person -> person.getAge() > 25)
等价。
需要注意的是这里的key都是Boolean
。