介绍 Java 8 groupingBy Collector
本文我们探讨下Java 8 groupingBy Collector,通过不同的示例进行详细讲解。
GroupingBy Collector
Java 8 Stream API 提供了声明方式处理流数据。static工厂方法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)
下面通过示例详细看看每个方法的应用场景。
基础准备
为了演示groupingBy(),让我们定义BlogPost类(作为流使用):
class BlogPost {
String title;
String author;
BlogPostType type;
int likes;
}
BlogPostType:
enum BlogPostType {
NEWS,
REVIEW,
GUIDE
}
定义一个BlogPost列表:
List<BlogPost> posts = Arrays.asList( ... );
同时也订一个Tuple类用于根据多个属性进行分组:
class Tuple {
BlogPostType type;
String author;
}
通过单个属性简单分组
首先从最简单的groupingBy 方法开始,使用一个分类函数作为参数。分类函数应用于流中的每个元素。函数的返回值用于map的key,映射至分组集合。根据post类型进行分组的代码:
Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType));
根据复杂类型进行分组
Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));
分组类型不限于标量或字符串类型。map的key可以是任何对象类型,只有我们确保实现必要的equals 和 hashcode 方法。下面根据组合类型进行分组:
Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));
修改返回Map值类型
第二个重载groupingBy方法带另一个参数指定后续收集器,应用于第一个集合结果。当我们仅指定一个分类器函数,没有后续收集器,则返回toList()集合。如何后续收集器使用toSet(),则会获得Set集合,而不是List:
Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, toSet()));
根据多个字段进行分组
与应用后续收集器不同的是,可以指定第二个分类器对第一个分组结果再分组。对BlogPost类的List根据作者和类型进行分组代码如下:
Map<String, Map<BlogPostType, List>> map = posts.stream()
.collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));
获取分组结果的平均值
通过使用后续收集器可以对分组函数的结果使用聚集函数。获取每种blog类型的平均数:
Map<BlogPostType, Double> averageLikesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes)));
获取分组结果之和
计算每个分组之和:
Map<BlogPostType, Integer> likesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));
获取分组结果的最大值和最小值
另外可以通过聚集函数获取最大数量的blog类型:
Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream()
.collect(groupingBy(BlogPost::getType,
maxBy(comparingInt(BlogPost::getLikes))));
类似的,我们能应用minBy后续收集器获得最小数量的分类。注意,maxBy和minBy 收集器考虑到应用它的集合可能是空的。这就是为什么map中的值类型是可选的。
获取属性分组结果的摘要信息
Collectors API提供了摘要收集器,用于需要同时获取数值属性的count,sum, minimum, maximum ,average 值。下面计算每种类型的摘要信息:
Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
summarizingInt(BlogPost::getLikes)));
每个类型的IntSummaryStatistics 对象包括属性的 count, sum, average, min 和 max值。另外摘要对象也有double和long类型。
映射分组结果至不同类型
更复杂的聚集可以对分类结果应用后续映射收集器。下面获得每个类型的连接blog的标题。
Map<BlogPostType, String> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]"))));
上面代码实现映射每个BlogPost 实例至title,然后reduce 文章标题流至连接字符串。本例Map的值是字符串,而不是默认List类型。
修改返回 Map 类型
当使用groupingBy 收集器,我们不能确定返回Map的类型。如果我们想指定特定Map类型作为返回值,我们使用三个参数的groupingBy 方法,通过提供Map supplier函数,其允许我们改变Map的类型。
通过EnumMap supplier函数给goupingBy方法获取EnumMap:
EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
() -> new EnumMap<>(BlogPostType.class), toList()));
并发Grouping By Collector
类似于 groupingBy, 也有groupingByConcurrent 收集器,其利用多线程架构。其有三个重载方法,带有与groupingBy一样的参数。然而,groupingByConcurrent 收集器的返回值必须是ConcurrentHashMap 类或其子类。
实现并发分组的代码如下:
ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()
.collect(groupingByConcurrent(BlogPost::getType));
如果你选择传递Map supplier函数给groupingByConcurrent 收集器,那么需要确保函数返回值也必须是ConcurrentHashMap 类或其子类。
总结
本文我们看了Java 8 中提供的几个groupingBy collector示例。groupingBy可以对流元素根据其属性进行分组,然后进一步收集、改变并收集至最终的容器中。