如何用Java案例实现数据分组?

wen java案例 4

如何用Java案例实现数据分组?从Stream API到自定义分组器的完整实践

目录导读

  1. 数据分组的核心需求与场景
  2. Java 8 Stream API:最优雅的“一行代码”分组
  3. 多级分组:按多个字段嵌套分组
  4. 自定义分组逻辑:按条件范围分组
  5. Map+Lambda:手动实现分组(兼容旧版本)
  6. 性能与陷阱:并发分组与Null处理
  7. QA:常见问题与最佳实践

数据分组的核心需求与场景

在Java开发中,“数据分组”是极其频繁的操作。

如何用Java案例实现数据分组?

  • 电商系统:按订单状态分组统计;
  • 报表系统:按月份、部门分组汇总;
  • 用户管理:按等级、地区分组展示。

核心目标:将原始List集合,基于某个(或某几个)属性,转换为Map<K, List>结构。
Java 8引入的Collectors.groupingBy()让这一操作从“手工循环”变为“一行Lambda”。


Java 8 Stream API:最优雅的“一行代码”分组

案例:学生按年级分组

public class Student {
    private String name;
    private int grade;   // 年级
    private double score;
    // 省略构造/getter/setter
}
List<Student> students = Arrays.asList(
    new Student("Alice", 1, 89.5),
    new Student("Bob", 2, 76.0),
    new Student("Cathy", 1, 92.3)
);
Map<Integer, List<Student>> groupByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade));
// 输出:{1=[Alice, Cathy], 2=[Bob]}

关键点groupingBy(Function)接受一个分类器函数(通常是getter方法引用),自动生成Map。

进阶:分组后计数、求和

// 统计每个年级人数
Map<Integer, Long> countByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
// 分组后求和(每个年级总分)
Map<Integer, Double> sumByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade, 
             Collectors.summingDouble(Student::getScore)));

多级分组:按多个字段嵌套分组

有时需要先按年级分组,再按性别分组。groupingBy可以嵌套:

Map<Integer, Map<String, List<Student>>> multiGroup = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade,
             Collectors.groupingBy(Student::getGender)));
// 结果:{1={Male=[...], Female=[...]}, 2={Male=[...]}}

注意:多级分组会产生嵌套Map,访问时需两层key,适合复杂报表。


自定义分组逻辑:按条件范围分组

当分组依据不是简单属性,而是“范围判断”时,需自定义分类器。

案例:按成绩等级分组(优秀/及格/不及格)

Map<String, List<Student>> groupByScoreLevel = students.stream()
    .collect(Collectors.groupingBy(s -> {
        if (s.getScore() >= 90) return "优秀";
        else if (s.getScore() >= 60) return "及格";
        else return "不及格";
    }));

核心:分类器函数可以包含任意逻辑(if-else、switch、正则等)。

踩坑提醒:分组字段为Null

若分组字段可能为Null,分组会抛出NullPointerException
解决方案:使用Collectors.groupingBy(..., HashMap::new, downstream)并提前处理null,或改用Collectors.mapping配合Optional


Map+Lambda:手动实现分组(兼容旧版本)

若不使用Java8(如Android低版本),可手动循环分组:

Map<Integer, List<Student>> result = new HashMap<>();
for (Student s : students) {
    // 方式1:computeIfAbsent(Java8+)
    result.computeIfAbsent(s.getGrade(), k -> new ArrayList<>()).add(s);
    // 方式2:传统写法(兼容Java7)
    if (!result.containsKey(s.getGrade())) {
        result.put(s.getGrade(), new ArrayList<>());
    }
    result.get(s.getGrade()).add(s);
}

computeIfAbsent是Java8新增,但若允许Java8,请优先用Stream API,代码更简洁。


性能与陷阱:并发分组与Null处理

性能建议

  • 单核场景:Stream API性能与手动循环相近,建议直接使用;
  • 大集合(百万级):考虑parallelStream() + groupingByConcurrent(),但需注意线程安全。

常见陷阱

  1. 分组Map的key顺序
    groupingBy默认返回HashMap(无序),若需有序(如按年级升序),使用groupingBy(..., TreeMap::new, downstream)
  2. 分组后值未排序
    groupingBy按集合出现顺序保留元素,若要排序,请在分组前对Stream排序,或使用LinkedHashMap接收。
  3. 空集合的分组
    若List为空,分组结果Map也为空,不会报错。

QA:常见问题与最佳实践

Q1:分组后如何获取每个分组的平均分?

Map<Integer, Double> avgByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade, 
             Collectors.averagingDouble(Student::getScore)));

Q2:分组后如何只保留每个分组的前2个元素?

Map<Integer, List<Student>> top2 = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade,
             Collectors.collectingAndThen(
                 Collectors.toList(),
                 list -> list.stream().limit(2).collect(Collectors.toList())
             )));

Q3:分组后转为特定Map类型(如LinkedHashMap保持插入顺序)?

Map<Integer, List<Student>> ordered = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade, 
             LinkedHashMap::new,  // 指定Map类型
             Collectors.toList()));

Java中实现数据分组,推荐优先使用Stream API的groupingBy,它支持:

  • 单字段/多字段分组
  • 自定义分组逻辑(条件、范围)
  • 分组后聚合(计数、求和、平均)
  • 指定Map类型(TreeMap、LinkedHashMap)

对于旧版本或特殊需求,用Map.computeIfAbsent或手动循环也能实现,唯一需要警惕的是Null分组字段大集合的并发性能

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