StreamAPI分组统计怎样操作

wen java案例 2

Stream API分组统计操作指南:从入门到实战

目录导读

  1. 什么是Stream API分组统计?
  2. 分组统计的核心方法:Collectors.groupingBy()
  3. 基础分组操作:根据单个字段分组
  4. 多级分组:GroupingBy嵌套实现
  5. 分组后统计:计数、求和、平均值
  6. 自定义分组条件:使用Lambda表达式
  7. 分组与汇总结合:下游收集器详解
  8. 实际案例:电商订单分组统计
  9. 注意事项与性能优化

什么是Stream API分组统计?

在Java 8引入的Stream API中,分组统计是指将集合中的元素按照某个或多个属性进行分类,并对每个分组进行聚合计算(如计数、求和、求平均值等)的操作,它类似于SQL中的GROUP BY语句,但使用函数式编程风格,代码更简洁、可读性更强。

StreamAPI分组统计怎样操作

问:Stream分组统计与传统的for循环分组相比,优势在哪里? 答:Stream API利用内部迭代和并行流机制,代码量减少约60%,且天然支持多线程并行处理,在大数据量场景下性能提升显著,更重要的是,分组统计通过声明式编程让业务逻辑更加清晰。


分组统计的核心方法:Collectors.groupingBy()

Collectors.groupingBy()是分组统计的"发动机",它接收一个分类函数,返回一个Map,其中键是分组类别,值是属于该组的元素列表。

基本语法:

Map<K, List<T>> map = list.stream().collect(Collectors.groupingBy(T::getField));
  • K:分组键的类型
  • T:集合元素的类型
  • T::getField:提取分组字段的方法引用

基础分组操作:根据单个字段分组

假设我们有一个Student类,包含namegrade(年级)和score属性,现在需要按年级分组:

List<Student> students = Arrays.asList(
    new Student("张三", 1, 85),
    new Student("李四", 2, 92),
    new Student("王五", 1, 78),
    new Student("赵六", 3, 88)
);
Map<Integer, List<Student>> groupByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade));
// 输出结果
// {1=[张三(85), 王五(78)], 2=[李四(92)], 3=[赵六(88)]}

问:如果分组字段为null会怎样? 答:默认情况会抛出NullPointerException,建议使用groupingBy(keyMapper, HashMap::new, downstream)重载方法,或者确保分组字段非空。


多级分组:GroupingBy嵌套实现

有时需要同时按多个维度分组,例如先按年级分组,再按成绩等级分组:

Map<Integer, Map<String, List<Student>>> multiGroup = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.groupingBy(student -> student.getScore() >= 90 ? "优秀" : "一般")
    ));
// 输出结构:{1:{一般=[张三, 王五]}, 2:{优秀=[李四]}, 3:{一般=[赵六]}}

这里内层的groupingBy就是一个下游收集器(downstream collector),它负责对每个分组内的元素进行二次分组。


分组后统计:计数、求和、平均值

分组统计的精髓在于下游汇总,通过配合不同的Collectors,可以实现丰富的统计功能。

1 统计每个组的人数

Map<Integer, Long> countByGrade = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade, 
        Collectors.counting()
    ));
// {1=2, 2=1, 3=1}

2 计算每组的总分

Map<Integer, Integer> sumByGrade = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.summingInt(Student::getScore)
    ));
// {1=163, 2=92, 3=88}

3 计算每组的平均分

Map<Integer, Double> avgByGrade = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.averagingDouble(Student::getScore)
    ));
// {1=81.5, 2=92.0, 3=88.0}

4 同时获取多项统计值

Map<Integer, IntSummaryStatistics> stats = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.summarizingInt(Student::getScore)
    ));
// 每个分组都包含count、sum、min、max、average

问:Collectors.summarizingInt()返回的IntSummaryStatistics能直接获取最大值吗? 答:可以,通过stats.get(1).getMax()即可获得年级1的最高分。


自定义分组条件:使用Lambda表达式

当分组逻辑不依赖于对象自身的getter时,可以使用Lambda自定义分组键:

// 按分数段分组:<60不及格,60-80良好,>80优秀
Map<String, List<Student>> scoreLevel = students.stream()
    .collect(Collectors.groupingBy(s -> {
        if (s.getScore() < 60) return "不及格";
        else if (s.getScore() <= 80) return "良好";
        else return "优秀";
    }));

或者使用更复杂的条件,例如按名字长度分组:

Map<Integer, List<Student>> byNameLength = students.stream()
    .collect(Collectors.groupingBy(s -> s.getName().length()));

分组与汇总结合:下游收集器详解

groupingBy的完整签名为:

groupingBy(Function<? super T, ? extends K> classifier, 
           Collector<? super T, A, D> downstream)

常见下游收集器组合:

下游收集器 功能 示例
toList() 默认行为,返回List 分组后获取列表
toSet() 去重分组 去除组内重复元素
counting() 计数 每组元素个数
summingInt() 求和 每组某个字段总和
averagingDouble() 平均值 每组某字段平均值
maxBy() 最大值 每组记录中最大元素
minBy() 最小值 每组记录中最小元素
mapping() 先转换再收集 提取特定字段到新集合

示例:找出每组分数最高的学生

Map<Integer, Optional<Student>> topStudent = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.maxBy(Comparator.comparing(Student::getScore))
    ));

示例:将每组的学生姓名收集为列表

Map<Integer, List<String>> nameByGrade = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
// {1=[张三, 王五], 2=[李四], 3=[赵六]}

实际案例:电商订单分组统计

假设我们有一个电商系统,需要统计每个用户的订单汇总信息:

public class Order {
    private Long userId;
    private String category;
    private BigDecimal amount;
    private LocalDateTime createTime;
}
List<Order> orders = getOrders(); // 获取订单数据
// 需求1:统计每个用户的订单总金额和订单数量
Map<Long, Map<String, Object>> userStats = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getUserId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                BigDecimal totalAmount = list.stream()
                    .map(Order::getAmount)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
                return Map.of("count", list.size(), "totalAmount", totalAmount);
            }
        )
    ));
// 需求2:统计每个商品类别的销售金额占比
Map<String, BigDecimal> categorySales = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getCategory,
        Collectors.mapping(
            Order::getAmount,
            Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
        )
    ));
BigDecimal totalSales = categorySales.values().stream()
    .reduce(BigDecimal.ZERO, BigDecimal::add);
// 计算占比
Map<String, Double> percentage = categorySales.entrySet().stream()
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        e -> e.getValue().divide(totalSales, 4, RoundingMode.HALF_UP).doubleValue()
    ));

问:在分组统计时,如果数据集很大(百万级),如何优化? 答:1)使用parallelStream()开启并行流 2)手动指定HashMap初始容量减少扩容 3)使用groupingByConcurrent()支持并发 4)避免在Lambda内进行重量级I/O操作。


注意事项与性能优化

常见陷阱

  1. Null分组键:如果分组字段可能为null,使用groupingBy重载方法传入Supplier指定Map类型。
  2. 未排序的Map:默认返回的是HashMap,不保证顺序,如需有序分组,使用groupingBy(classifier, TreeMap::new, downstream)
  3. 并行流线程安全:并行流分组建议使用groupingByConcurrent()

性能优化建议

// 优化前:默认HashMap可能频繁扩容
Map<Integer, List<Student>> result = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade));
// 优化后:预估容量,减少rehash
int estimatedSize = students.size() / 10 + 1; // 假设平均每组10人
Map<Integer, List<Student>> result = students.stream()
    .collect(Collectors.groupingBy(
        Student::getGrade,
        () -> new HashMap<>(estimatedSize),
        Collectors.toList()
    ));

与SQL的对比

操作 Stream API SQL
分组 groupingBy() GROUP BY
计数 counting() COUNT(*)
求和 summingInt() SUM()
最大值 maxBy() MAX()
多级分组 嵌套groupingBy GROUP BY a, b

Stream API的分组统计功能通过函数式编程,将复杂的数据分组和聚合任务变得优雅高效,掌握groupingBy与各种下游收集器的组合,能让你在处理集合数据时事半功倍,建议在实际开发中,先用简单分组验证逻辑,再逐步添加统计汇总,最后根据数据量进行必要的性能优化。

关键点回顾

  • groupingBy(keyMapper) 是分组基础
  • 下游收集器决定分组后做什么
  • counting() summingInt() averagingDouble() 是常用统计
  • 自定义分组条件用Lambda表达式
  • 大数据量考虑并行流和Map容量优化

上一篇Optional如何避免空指针异常

下一篇当前分类已是最新一篇

抱歉,评论功能暂时关闭!