黑客日教程-Java8新功能:将数据集合进行分组,类似SQL的GROUP BY

1 介绍

本文将展示groupingBy收集器的多个示例。
阅读本文需要先准备Java Stream和Java收集器Collector的知识。

2 GroupingBy收集器

Java8的Stream API允许我们以声明的方式来处理数据集合。
静态工厂方法:Collectors.groupingBy(),以及Collectors.groupingByConcunrrent(),给我们提供了类似SQL语句中的"GROUP BY"的功能。这两个方法将数据按某些属性分组,并存储在Map中返回。
下面是几个重载的groupnigBy方法:

  • 参数:分类函数
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 准备

先定义一个BlogPost类:

class BlogPost {
    String title;
    String author;
    BlogPostType type;
    int likes;
}

BlogPostType:

enum BlogPostType {
    NEWS,
    REVIEW,
    GUIDE
}

BlogPost列表:

List<BlogPost> posts = Arrays.asList( ... );

2.2 根据单一字段分组

最简单的groupingBy方法,只有一个分类函数做参数。分类函数作用于strema里面的每个元素。分类函数处理后返回的每个元素作为返回Map的key。
根据博客文章类型来分组:

Map<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
  .collect(groupingBy(BlogPost::getType));

2.3 根据Map的key的类型分组

分类函数并没有限制返回字符串或标量值。返回map的key可以是任何对象。只要实现了其equals和hashcode方法。
下面示例根据type和author组合而成的Tuple实例来排序:

Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
  .collect(groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));

2.4 修改返回Map的value的类型

groupingBy的第二个重载方法有一个额外的collector参数(downstream),此参数作用于第一个collector产生的结果。
如果只用一个分类函数做参数,那么默认会使用toList()这个collector来转换结果。
下面的代码显示地使用了toSet()这个collector传递给downstream这个参数,因此会得到一个博客文章的Set。

Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream()
  .collect(groupingBy(BlogPost::getType, toSet()));

2.5 根据多个字段分组

downstream参数的另外一个用处就是基于分组结果,做第二次分组。
下面代码,首先根据author分组,然后再根据type分组:

Map<String, Map<BlogPostType, List>> map = posts.stream()
  .collect(groupingBy(BlogPost::getAuthor, groupingBy(BlogPost::getType)));

2.6 得到分组结果的平均值

通过使用downstream,我们可以把集合函数应用到第一次分组的结果上。比如,获取到每种类型博客的被喜欢次数(likes)的平均值:

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))));

类似的,可以用minxBy得到每种类型博客中被喜欢次数最少的次数是多少。
注意:maxBy和minBy都考虑了当第一次分组得到的结果是空的场景,因此其返回结果(Map的value)是Optional<BlogPost>。

2.9 得到分组结果中某个属性的统计

Collectors API提供了一个统计collector,可以用来同时计算数量、总计、最小值、最大值、平均值等。
下面来统计一下不同类型博客的被喜欢(likes)这个属性:

Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream()
  .collect(groupingBy(BlogPost::getType, 
  summarizingInt(BlogPost::getLikes)));

返回Map中的value,IntSummaryStatistics对象,包括了每个BlogPostType的文章次数、被喜欢总计、平均值、最大值、最小值。

2.10 把分组结果映射为另外的类型

更复杂的聚合操作可以通过应用一个映射downstream收集器到分类函数结果上来实现。
下面代码讲每类博客类型的标题连接起来了。

Map<BlogPostType, String> postsPerType = posts.stream()
  .collect(groupingBy(BlogPost::getType, 
  mapping(BlogPost::getTitle, joining(", ", "Post titles: [", "]"))));

上面的代码,讲每个BlogPost实例映射为了其对应的标题,然后把博客标题的stream连接成了成了字符串,形如“Post titles:[标题1,标题2,标题3]”。

2.11 修改返回Map的类型

使用groupingBy的时候,如果我们要指定返回Map的具体类型,可以用第三个重载方法。通过传入一个Map供应者函数。
下面代码传入了一个EnumMap供应者函数,得到返回Map为EnumMap类型。

EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
  .collect(groupingBy(BlogPost::getType, 
  () -> new EnumMap<>(BlogPostType.class), toList()));

3 并发的分组Collector

类似groupingBy,存在一个groupingByConcurrent收集器,可以利用到多核架构的能力。groupingByConcurrent也有3个重载的方法,与groupingBy类似。
但返回值必须是ConconcurrentHashMap或其子类。
要并发操作分组,那么stream也必须是并行的:

ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()
  .collect(groupingByConcurrent(BlogPost::getType));

注意:如果要提供一个Map供应者函数,必须保证函数返回的是ConconcurrentHashMap或其子类。

4 Java 9新功能

java9引入两个新的收集器可以在goupingBy中使用的:更多详情

5 小结

本文讨论了Java 8 Collectors API中的groupingBy收集器的几个例子。
讨论了goupingBy如何对stream中的元素基于某个属性进行分组,以及如何返回结果。
示例代码见github

编译

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容