1.介绍
使用各种示例来了解groupingBy收集器的工作方式。
2.groupingBy Collectors
Java 8 Stream API能够以声明的方式处理数据集合。
静态工厂方法Collectors.groupingBy()和Collectors.groupingByConcurrent()提供了类似于SQL语言中“ GROUP BY”子句的功能。 使用它们将对象按某些属性分组,并将结果存储在Map实例中。
groupingBy的重载方法是
- 首先,以分类函数作为方法参数:
static <T,K> Collector<T,?,Map<K,List<T>>>
groupingBy(Function<? super T,? extends K> classifier)
- 其次,使用分类函数和第二个收集器作为方法参数:
static <T,K,A,D> Collector<T,?,Map<K,D>>
groupingBy(Function<? super T,? extends K> classifier,
Collector<? super T,A,D> downstream)
- 最后,通过分类函数,提供者方法(提供包含最终结果的Map实现)和第二个收集器作为方法参数
static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M>
groupingBy(Function<? super T,? extends K> classifier,
Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
2.1示例代码准备
为了演示groupingBy()的用法,让定义一个BlogPost类(将使用BlogPost对象流):
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BlogPost {
String title;
String author;
BlogPostType type;
int likes;
}
public enum BlogPostType {
/**
*
*/
NEWS,
REVIEW,
GUIDE
}
定义清单
static List<BlogPost> posts = new ArrayList<>();
static {
posts.add(new BlogPost("map使用用法", "张三", BlogPostType.NEWS,1));
posts.add(new BlogPost("List使用用法", "李四", BlogPostType.GUIDE,1));
posts.add(new BlogPost("Set使用用法", "张三", BlogPostType.REVIEW,1));
posts.add(new BlogPost("Stream使用用法", "赵六", BlogPostType.NEWS,1));
posts.add(new BlogPost("Thread使用用法", "田七", BlogPostType.GUIDE,1));
}
还定义一个Tuple类,该类将用于通过组合其类型和作者属性来对帖子进行分组:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Tuple {
BlogPostType type;
String author;
}
2.2 通过单列进行简单分组
从最简单的groupingBy方法开始,该方法仅将分类函数作为其参数。 分类函数应用于流的每个元素。 使用函数返回的值作为从groupingBy收集器获取的映射的键。
要将博客文章按其类型分组在博客文章列表中,请执行以下操作:
@Test
public void test1(){
Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType));
System.out.println(postsPerType);
}
输出如下:
{REVIEW=[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)], NEWS=[BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1), BlogPost(title=Stream使用用法, author=赵六, type=NEWS, likes=1)], GUIDE=[BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1), BlogPost(title=Thread使用用法, author=田七, type=GUIDE, likes=1)]}
2.3通过复杂映射键类型进行分组
分类函数不限于仅返回标量或String值。 只要确保实现必要的equals和hashcode方法,结果映射的键就可以是任何对象。
要将列表中的博客文章按类型和作者(在Tuple实例中组合)进行分组:
@Test
public void test2(){
Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));
System.out.println(postsPerTypeAndAuthor);
}
输出
{ Tuple(type=NEWS, author=赵六)=[BlogPost(title=Stream使用用法, author=赵六, type=NEWS, likes=1)],
Tuple(type=NEWS, author=张三)=[BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1)],
Tuple(type=GUIDE, author=李四)=[BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1)],
Tuple(type=GUIDE, author=田七)=[BlogPost(title=Thread使用用法, author=田七, type=GUIDE, likes=1)],
Tuple(type=REVIEW, author=张三)=[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)]}
2.4修改返回的Map值类型
groupingBy的第二次重载使用了一个附加的第二收集器(下游收集器),该收集器应用于第一收集器的结果。
指定分类函数但不指定下游收集器时,将在后台使用toList()收集器。
使用toSet()收集器作为下游收集器,并获取一组博客文章(而不是List):
@Test
public void test3(){
Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, toSet()));
System.out.println(postsPerType);
}
输出
{REVIEW=[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)],
NEWS=[BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1),
BlogPost(title=Stream使用用法, author=赵六, type=NEWS, likes=1)],
GUIDE=[BlogPost(title=Thread使用用法, author=田七, type=GUIDE, likes=1),
BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1)]}
2.5按多个字段分组
下游收集器的另一种应用是对第一组的结果进行二级分组。
要按作者然后按类型对BlogPost列表进行分组:
@Test
public void test4(){
Map<String, Map<BlogPostType, List<BlogPost>>> map = posts.stream()
.collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));
System.out.println(map);
}
输出
{李四={GUIDE=[BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1)]},
张三={REVIEW=[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)], NEWS=[BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1)]},
赵六={NEWS=[BlogPost(title=Stream使用用法, author=赵六, type=NEWS, likes=1)]},
田七={GUIDE=[BlogPost(title=Thread使用用法, author=田七, type=GUIDE, likes=1)]}}
2.6从分组结果中获取平均值
通过使用下游收集器,可以将聚合函数应用到分类函数的结果中。
例如,要查找每种博客文章类型的平均点赞次数:
@Test
public void test5(){
Map<BlogPostType, Double> averageLikesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes)));
System.out.println(averageLikesPerType);
}
输出
{NEWS=1.0, GUIDE=1.0, REVIEW=1.0}
2.7从分组结果中获取总和
要计算每种类型的喜欢总数:
@Test
public void test6(){
Map<BlogPostType, Integer> likesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));
System.out.println(likesPerType);
}
输出
{NEWS=2, GUIDE=2, REVIEW=1}
2.8从分组结果中获取最大值或最小值
可以执行的另一种汇总方式是获得具有最多“赞”次数的博客帖子:
@Test
public void test7(){
Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream()
.collect(groupingBy(BlogPost::getType,
maxBy(comparingInt(BlogPost::getLikes))));
System.out.println(maxLikesPerPostType);
}
输出
{
GUIDE=Optional[BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1)],
NEWS=Optional[BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1)],
REVIEW=Optional[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)]
}
同样,我们可以应用minBy下游收集器来获得具有最少点赞次数的博客文章。
请注意,maxBy和minBy收集器考虑了应用它们的集合可能为空的可能性。 这就是为什么映射中的值类型为Optional 的原因。
2.9获取分组结果属性的总和
Collectors API提供了一个汇总收集器,可以在需要同时计算数值属性的计数,总和,最小值,最大值和平均值的情况下使用。
每种不同类型的博客文章的likes属性计算一个总和:
@Test
public void test8(){
Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
summarizingInt(BlogPost::getLikes)));
System.out.println(likeStatisticsPerType);
}
输出
{
REVIEW=IntSummaryStatistics{count=1, sum=1, min=1, average=1.000000, max=1},
NEWS=IntSummaryStatistics{count=2, sum=2, min=1, average=1.000000, max=1},
GUIDE=IntSummaryStatistics{count=2, sum=2, min=1, average=1.000000, max=1}
}
2.10将分组结果映射到其他类型
通过将映射下游收集器应用于分类函数的结果,可以实现更复杂的聚合。
连接每种博客文章类型的文章标题:
@Test
public void test9(){
Map<BlogPostType, String> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]"))));
System.out.println(postsPerType);
}
输出
{
REVIEW=Post titles: [Set使用用法],
NEWS=Post titles: [map使用用法, Stream使用用法],
GUIDE=Post titles: [List使用用法, Thread使用用法]
}
2.11修改返回Map类型
使用groupingBy收集器时,无法对返回的Map的类型进行假设。 如果要具体确定要从分组依据中获取哪种类型的地图,则可以使用groupingBy方法的第三个变体,该方法允许通过传递Map类型来更改地图的类型。
接下来将EnumMap提供程序函数传递给groupingBy方法来检索EnumMap:
@Test
public void test10(){
EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
() -> new EnumMap<>(BlogPostType.class), toList()));
System.out.println(postsPerType);
}
输出:
{
NEWS=[BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1), BlogPost(title=Stream使用用法, author=赵六, type=NEWS, likes=1)],
REVIEW=[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)],
GUIDE=[BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1), BlogPost(title=Thread使用用法, author=田七, type=GUIDE, likes=1)]
}
2.12分组返回并发收集器
与groupingBy类似的是groupingByConcurrent收集器,该收集器利用了多核体系结构。 该收集器具有三个重载方法,它们采用与groupingBy收集器的各个重载方法完全相同的参数。 但是,groupingByConcurrent收集器的返回类型必须是ConcurrentHashMap类的实例或其子类。
要同时执行分组操作,流必须是并行的:
@Test
public void test11(){
ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()
.collect(groupingByConcurrent(BlogPost::getType));
System.out.println(postsPerType);
}
输出
{
GUIDE=[BlogPost(title=Thread使用用法, author=田七, type=GUIDE, likes=1), BlogPost(title=List使用用法, author=李四, type=GUIDE, likes=1)],
NEWS=[BlogPost(title=Stream使用用法, author=赵六, type=NEWS, likes=1), BlogPost(title=map使用用法, author=张三, type=NEWS, likes=1)], REVIEW=[BlogPost(title=Set使用用法, author=张三, type=REVIEW, likes=1)]
}