文章目录
前言
一、GroupingBy收集器
二、使用示例
2.1 准备
2.2 根据单一字段分组
2.3 根据Map的key的类型分组
2.4 修改返回Map的value的类型
2.5 修改返回自定义类型
2.6 根据多个字段分组
2.7 得到分组结果的平均值
2.8 得到分组结果的总计
2.9 得到分组结果中的最大或最小值
2.10 得到分组结果中某个属性的统计
2.11 把分组结果映射为另外的类型
2.12 修改返回Map的类型
2.13 collectingAndThen包裹一个收集器,对其结果应用转换函数
3 并发的分组Collector
总结
前言
本文将展示groupingBy收集器的多个示例,阅读本文需要先准备Java Stream和Java收集器Collector的知识。

一、GroupingBy收集器
Java8的Stream API允许我们以声明的方式来处理数据集合。

静态工厂方法:Collectors.groupingBy(),以及Collectors.groupingByConcunrrent(),给我们提供了类似SQL语句中的"GROUP BY"的功能。这两个方法将数据按某些属性分组,并存储在Map中返回。

作为collect方法的参数,Collector是一个接口,它是一个可变的汇聚操作,将输入元素累计到一个可变的结果容器中;它会在所有元素都处理完毕后,将累积的结果转换为一个最终的表示(这是一个可选操作);

Collectors本身提供了关于Collector的常见汇聚实现,Collectors的内部类CollectorImpl实现了Collector接口,Collectors本身实际上是一个工厂。
Collector 类方法:

public interface Collector<T, A, R> { 
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();

    Set<Characteristics> characteristics();
}

Collector主要定义了容器的类型,添加元素的方法,容器合并的方法还有输出的结果。

supplier就是生成容器
accumulator是添加元素
combiner是合并容器
finisher是输出的结果
characteristics是定义容器的三个属性(三个枚举值),包括是否有明确的finisher,是否需要同步,是否有序。
CONCURRENT(集合的操作需要同步):表示中间结果只有一个,即使在并行流的情况下。所以只有在并行流且收集器不具备CONCURRENT特性时,combiner方法返回的lambda表达式才会执行(中间结果容器只有一个就无需合并)
UNORDER(集合是无序的):表示流中的元素无序。
IDENTITY_FINISH(不用finisher):表示中间结果容器类型与最终结果类型一致,此时finiser方法不会被调用
其中这里的泛型所表示的含义是:
T:表示流中每个元素的类型。
A:表示中间结果容器的类型。
R:表示最终返回的结果类型。

下面是几个重载的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)
1
2
3
参数:分类函数,供应者方法(提供作为返回值的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
}

public class Tuple {
    String author;

    BlogPostType type;

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public BlogPostType getType() {
        return type;
    }

    public void setType(BlogPostType type) {
        this.type = type;
    }
}

BlogPost列表:

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

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

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

groupingBy(BlogPost::getType) 默认返回的是toList。

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

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

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 修改返回自定义类型
mapping函数:mapper:返回参数对象,downstream收集的集合值。

Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
                               Collector<? super U, A, R> downstream)

示例代码:

Map<BlogPostType, List<Map<String, Object>>> postsPerType = posts.stream()
                .collect(groupingBy(BlogPost::getType, mapping(n->getBlogPost(n), toList())));
                
 privatestatic Map<String, Object> getBlogPost(BlogPost blogPost) {
        Map<String, Object> map = new HashMap<>();
        map.put("title", blogPost.getTitle());
        return map;
    }

mapping() 收集器,自定义返回。

2.6 根据多个字段分组
downstream参数的另外一个用处就是基于分组结果,做第二次分组。

下面代码,首先根据author分组,然后再根据type分组:

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

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


Map<BlogPostType, Double> averageLikesPerType = posts.stream()

.collect(groupingBy(BlogPost::getType, averagingInt(BlogPost::getLikes)));


2.8 得到分组结果的总计
计算每种类型被喜欢次数的总数:


Map<BlogPostType, Integer> likesPerType = posts.stream()

.collect(groupingBy(BlogPost::getType, summingInt(BlogPost::getLikes)));


2.9 得到分组结果中的最大或最小值
我们还可以得到每种类型博客被喜欢次数最多的是多少:


Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream()

.collect(groupingBy(BlogPost::getType,

maxBy(comparingInt(BlogPost::getLikes))));


类似的,可以用minxBy得到每种类型博客中被喜欢次数最少的次数是多少。

注意:maxBy和minBy都考虑了当第一次分组得到的结果是空的场景,因此其返回结果(Map的value)是Optional<BlogPost>。

2.10 得到分组结果中某个属性的统计
Collectors API提供了一个统计collector,可以用来同时计算数量、总计、最小值、最大值、平均值等。

下面来统计一下不同类型博客的被喜欢(likes)这个属性:


Map<BlogPostType, IntSummaryStatistics> likeStatisticsPerType = posts.stream()

.collect(groupingBy(BlogPost::getType,

summarizingInt(BlogPost::getLikes)));


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

2.11 把分组结果映射为另外的类型
更复杂的聚合操作可以通过应用一个映射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.12 修改返回Map的类型
使用groupingBy的时候,如果我们要指定返回Map的具体类型,可以用第三个重载方法。通过传入一个Map供应者函数。

下面代码传入了一个EnumMap供应者函数,得到返回Map为EnumMap类型。


EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()

.collect(groupingBy(BlogPost::getType,

() -> new EnumMap<>(BlogPostType.class), toList()));


2.13 collectingAndThen包裹一个收集器,对其结果应用转换函数
public static <T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream,
Function<R,RR> finisher)
示例代码
//对分组进行转换,对分组内元素进行计算

Map<BlogPostType, Map<String, Object>> postsPerTypeList = posts.stream()
        .collect(groupingBy(BlogPost::getType, collectingAndThen(toList(), m->{
            Map<String, Object> map = new HashMap<>();
            map.put("count", m.stream().count());
            //对分组的list求和
            map.put("money", m.stream().mapToDouble(BlogPost::getLikes).sum());
            return map;
        })));

3 并发的分组Collector
类似groupingBy,存在一个groupingByConcurrent收集器,可以利用到多核架构的能力。groupingByConcurrent也有3个重载的方法,与groupingBy类似。

但返回值必须是ConconcurrentHashMap或其子类。

要并发操作分组,那么stream也必须是并行的:


ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()

.collect(groupingByConcurrent(BlogPost::getType));


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

java8有一个collectingAndThen可以根据多个字段去重

1

2

list.stream()

.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getProfessionId() + ";" + o.getGrade()))), ArrayList::new));

获取list集合中重复的元素

List<String> collect = buySettlement.getCosts().getValue().stream().map(cost -> cost.getBillingCostItem().getRefer().getName())
        .collect(Collectors.toMap(e -> e, e -> 1, Integer::sum))// 获得元素出现频率的 Map,键为元素,值为元素出现的次数
        .entrySet()
        .stream()// 所有 entry 对应的 Stream
        .filter(e -> e.getValue() > 1)// 过滤出元素出现次数大于 1 (重复元素)的 entry
        .map(Map.Entry::getKey)// 获得 entry 的键(重复元素)对应的 Stream
        .collect(Collectors.toList());// 转化为 List

总结
本文讨论了Java 8 Collectors API中的groupingBy收集器的几个例子。

讨论了goupingBy如何对stream中的元素基于某个属性进行分组,以及如何返回结果。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐