java8 stream统计、汇总、多字段分组、多个列汇总统计

文章目录
前言
一、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中的元素基于某个属性进行分组,以及如何返回结果。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你可以使用Java Stream的`collect()`方法结合`groupingBy()`和`summingInt()`来实现多个字段分组统计。 假设你有一个包含多个字段的对象表,你想要按照其中两个字段进行分组,并统计每个组中对象的数量。下面是一个示例代码: ```java import java.util.*; import java.util.stream.Collectors; class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } public class Main { public static void main(String[] args) { List<Person> people = Arrays.asList( new Person("Alice", 25), new Person("Bob", 30), new Person("Alice", 35), new Person("Bob", 40), new Person("Charlie", 20) ); Map<String, Map<Integer, Long>> result = people.stream() .collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy(Person::getAge, Collectors.counting()))); System.out.println(result); } } ``` 在上面的示例中,我们首先使用`groupingBy()`按照姓名字段对对象进行分组,然后在每个分组中再次使用`groupingBy()`按照年龄字段进行分组,并使用`counting()`统计每个分组中的对象数量。最后,将结果存储在一个嵌套的`Map`中。 输出结果将是一个类似于`{Alice={25=1, 35=1}, Bob={40=1, 30=1}, Charlie={20=1}}`的`Map`,其中外层的键是姓名,内层的键是年龄,值是对象数量。 你可以根据自己的需求修改代码来适应不同的字段统计方式。希望对你有帮助!如果还有其他问题,请随时问我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值