1. 引言
本教程将通过多个实例深入探讨 groupingBy 收集器的工作原理。要充分理解本文内容,你需要具备 Java 8 基础知识,建议先了解 Java 8 Stream 入门 和 Java 8 收集器指南。
2. groupingBy 收集器
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
对象流):
class BlogPost {
String title;
String author;
BlogPostType type;
int likes;
}
接着定义 BlogPostType
枚举:
enum BlogPostType {
NEWS,
REVIEW,
GUIDE
}
然后创建 BlogPost
对象列表:
List<BlogPost> posts = Arrays.asList( ... );
再定义一个 Tuple
类,用于按 type
和 author
属性组合分组:
class Tuple {
BlogPostType type;
String author;
}
2.2 按单列简单分组
从最简单的 groupingBy
方法开始,它仅接受一个分类函数作为参数。该函数会应用于流中的每个元素,返回值将作为结果 Map 的键。
按博客文章的 type
分组:
Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType));
2.3 复杂 Map 键类型的分组
分类函数的返回值不限于标量或字符串,结果 Map 的键可以是任何对象,只要正确实现 equals
和 hashCode
方法。
✅ 使用 Pair 类实现多字段分组(推荐 javafx.util
或 org.apache.commons.lang3.tuple
包):
Map<Pair<BlogPostType, String>, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new ImmutablePair<>(post.getType(), post.getAuthor())));
✅ 使用自定义 Tuple 类(更灵活,可扩展更多字段):
Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));
✅ Java 16+ 使用 record(更简洁安全):
public class BlogPost {
// ... 其他字段
record AuthPostTypesLikes(String author, BlogPostType type, int likes) {};
}
Map<BlogPost.AuthPostTypesLikes, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(groupingBy(post -> new BlogPost.AuthPostTypesLikes(post.getAuthor(), post.getType(), post.getLikes())));
2.4 修改返回的 Map 值类型
groupingBy
的第二个重载方法接受一个下游收集器(downstream collector),用于处理第一级分组的结果。若未指定下游收集器,默认使用 toList()
。
✅ **使用 toSet()
替代默认的 List
**:
Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, toSet()));
2.5 按多字段分组
通过嵌套使用下游收集器,可实现多级分组。先按 author
分组,再按 type
分组:
Map<String, Map<BlogPostType, List>> map = posts.stream()
.collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));
2.6 计算分组结果的平均值
利用下游收集器对分组结果进行聚合操作。计算每种博客文章类型的平均点赞数:
Map<BlogPostType, Double> averageLikesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes)));
2.7 计算分组结果的总和
计算每种类型的点赞总数:
Map<BlogPostType, Integer> likesPerType = posts.stream()
.collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));
2.8 获取分组结果的最大/最小值
获取每种类型中点赞数最多的文章:
Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream()
.collect(groupingBy(BlogPost::getType,
maxBy(comparingInt(BlogPost::getLikes))));
⚠️ 注意:maxBy
/minBy
返回 Optional
类型,因为分组结果可能为空。
2.9 获取分组属性的统计摘要
使用 summarizingInt
收集器同时计算计数、总和、平均值、最大值和最小值:
Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
summarizingInt(BlogPost::getLikes)));
2.10 聚合分组结果的多个属性
✅ 方案一:使用 collectingAndThen
先添加聚合结果封装类:
public class BlogPost {
// ...
record PostCountTitlesLikesStats(long postCount, String titles, IntSummaryStatistics likesStats){};
}
实现多属性聚合:
Map<String, BlogPost.PostCountTitlesLikesStats> postsPerAuthor = posts.stream()
.collect(groupingBy(BlogPost::getAuthor, collectingAndThen(toList(), list -> {
long count = list.stream().map(BlogPost::getTitle).collect(counting());
String titles = list.stream().map(BlogPost::getTitle).collect(joining(" : "));
IntSummaryStatistics summary = list.stream().collect(summarizingInt(BlogPost::getLikes));
return new BlogPost.PostCountTitlesLikesStats(count, titles, summary);
})));
✅ 方案二:使用 toMap
实现复杂聚合
定义结果封装类:
public class BlogPost {
// ...
record TitlesBoundedSumOfLikes(String titles, int boundedSumOfLikes) {};
}
实现带上限的点赞数聚合:
int maxValLikes = 17;
Map<String, BlogPost.TitlesBoundedSumOfLikes> postsPerAuthor = posts.stream()
.collect(toMap(BlogPost::getAuthor, post -> {
int likes = (post.getLikes() > maxValLikes) ? maxValLikes : post.getLikes();
return new BlogPost.TitlesBoundedSumOfLikes(post.getTitle(), likes);
}, (u1, u2) -> {
int likes = (u2.boundedSumOfLikes() > maxValLikes) ? maxValLikes : u2.boundedSumOfLikes();
return new BlogPost.TitlesBoundedSumOfLikes(
u1.titles().toUpperCase() + " : " + u2.titles().toUpperCase(),
u1.boundedSumOfLikes() + likes
);
}));
2.11 将分组结果映射为不同类型
使用 mapping
下游收集器转换分组结果的类型。将每种类型的文章标题拼接为字符串:
Map<BlogPostType, String> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]"))));
2.12 修改返回的 Map 类型
默认返回的 Map 类型不确定。若需指定具体实现(如 EnumMap
),使用第三个重载方法:
EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(groupingBy(BlogPost::getType,
() -> new EnumMap<>(BlogPostType.class), toList()));
3. 并发分组收集器
groupingByConcurrent
是 groupingBy
的并发版本,充分利用多核架构。其重载方法与 groupingBy
完全一致,但返回类型必须是 ConcurrentHashMap
或其子类。
⚠️ 使用前提:流必须是并行的(parallelStream()
):
ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()
.collect(groupingByConcurrent(BlogPost::getType));
4. Java 9 新增功能
Java 9 引入了两个与 groupingBy
配合良好的新收集器,详情见 Java 9 流收集器指南。
5. 总结
本文全面探讨了 Java 8 Collectors API 中 groupingBy
收集器的用法。我们学习了如何:
- 按属性对流元素分类
- 对分组结果进行收集、转换和聚合
- 处理复杂分组场景(多级分组、多属性聚合)
- 控制返回 Map 的类型
完整示例代码可在 GitHub 项目 中获取。