JDK系列05:JDK8 Stream流式编程,集合过滤、分组、求和、去重实战案例

📅 2026/7/2 20:34:35
JDK系列05:JDK8 Stream流式编程,集合过滤、分组、求和、去重实战案例
JDK系列05JDK8 Stream流式编程集合过滤、分组、求和、去重实战案例专栏系列JDK核心底层进阶系列05阅读前置零基础可入门无需函数式编程基础从传统代码痛点切入循序渐进掌握核心用法核心收获不讲空洞理论全程基于真实业务场景详解Stream最核心的过滤、分组、求和、去重四大核心操作附带大量可直接复制运行的代码、传统代码VS Stream代码对比、避坑指南新手也能一次性吃透一、Stream核心认知搞懂原理才能用得通透1.1 什么是Stream流Stream是JDK8新增的集合数据处理API并非新的集合容器也不存储数据它更像一个数据流水线。我们可以将集合、数组、IO流等数据源接入流水线通过一系列中间操作处理数据最后通过终端操作输出结果全程链式调用代码行云流水。1.2 Stream四大核心特性重点声明式编程只告诉程序“要什么结果”不用关心“怎么实现”告别循环迭代细节链式调用中间操作连续拼接逻辑连贯可读性拉满惰性求值所有中间操作过滤、映射等不会立即执行只有触发终端操作才会执行性能更高一次性使用Stream流一旦执行终端操作就会关闭无法重复使用1.3 Stream操作分类必记Stream所有操作只分为两类掌握这个分类就掌握了Stream的核心逻辑① 中间操作Intermediate处理数据、返回新Stream可链式拼接惰性执行。常用filter过滤、map映射、distinct去重、sorted排序、flatMap扁平化。② 终端操作Terminal触发计算、关闭流返回最终结果。常用collect收集、sum求和、count统计、forEach遍历、max/min最值。核心口诀中间操作叠buff终端操作放技能不放技能不生效1.4 前置准备通用实体类测试数据为了贴合真实业务本文所有案例统一使用员工实体类作为数据源覆盖日常开发90%集合操作场景所有代码可直接复制运行。/** * 员工实体类通用测试数据源 */ Data NoArgsConstructor AllArgsConstructor public class Employee { // 员工ID private Long id; // 员工姓名 private String name; // 所属部门 private String dept; // 年龄 private Integer age; // 薪资 private Double salary; // 性别 private String gender; }初始化测试数据/** * 初始化测试员工集合 */ public static ListEmployee getEmployeeList() { ListEmployee employeeList new ArrayList(); employeeList.add(new Employee(1L, 张三, 研发部, 24, 8500.0, 男)); employeeList.add(new Employee(2L, 李四, 研发部, 28, 12000.0, 男)); employeeList.add(new Employee(3L, 王五, 财务部, 26, 9500.0, 女)); employeeList.add(new Employee(4L, 赵六, 财务部, 30, 15000.0, 女)); employeeList.add(new Employee(5L, 孙七, 人事部, 25, 7500.0, 男)); employeeList.add(new Employee(6L, 周八, 人事部, 29, 8000.0, 女)); employeeList.add(new Employee(7L, 张三, 研发部, 27, 9000.0, 男)); return employeeList; }二、核心实战一Stream集合过滤filter过滤是Stream最常用的操作核心作用从集合中筛选出符合指定条件的元素剔除无效数据。对应SQL的where条件查询。2.1 传统for循环写法冗余需求筛选出研发部、薪资大于8000的员工// 传统for循环过滤 ListEmployee resultList new ArrayList(); for (Employee employee : getEmployeeList()) { // 双重条件过滤 if (研发部.equals(employee.getDept()) employee.getSalary() 8000) { resultList.add(employee); } } // 遍历结果 for (Employee employee : resultList) { System.out.println(employee.getName() employee.getSalary()); }缺点模板代码多、逻辑分散、多层if嵌套后可读性极差。2.2 Stream filter优雅写法// Stream流式过滤一行核心逻辑 ListEmployee streamFilterList getEmployeeList().stream() // 过滤条件部门为研发部 且 薪资大于8000 .filter(emp - 研发部.equals(emp.getDept()) emp.getSalary() 8000) // 收集结果为List集合 .collect(Collectors.toList()); // 结果遍历 streamFilterList.forEach(emp - System.out.println(emp.getName() emp.getSalary()));2.3 多条件、复杂过滤实战filter支持无限叠加条件可拆分多个filter逻辑更清晰// 多条件拆分过滤可读性更强 ListEmployee complexFilter getEmployeeList().stream() .filter(emp - emp.getAge() 30) // 条件1年龄小于30岁 .filter(emp - emp.getSalary() 8000) // 条件2薪资大于8000 .filter(emp - 女.equals(emp.getGender())) // 条件3女性员工 .collect(Collectors.toList());2.4 过滤避坑指南filter中判断字符串必须常量在前“研发部”.equals避免空指针异常filter只保留条件为true的元素多个条件建议拆分多个filter比单句拼接更易读、易维护三、核心实战二Stream集合去重distinct日常开发中集合去重是高频需求Stream提供distinct()快速去重同时支持对象属性去重解决默认去重局限性。3.1 基础类型集合去重默认去重// 基础类型去重字符串、数字 ListString nameList Arrays.asList(张三, 李四, 张三, 王五, 李四); ListString distinctName nameList.stream() .distinct() .collect(Collectors.toList()); System.out.println(基础类型去重结果 distinctName);3.2 对象集合去重核心难点Stream默认的distinct()基于equals和hashCode去重业务中我们常需要根据单个/多个属性去重比如根据员工姓名去重。/** * 对象属性去重根据姓名去重保留第一条数据 */ ListEmployee distinctEmp getEmployeeList().stream() .filter(distinctByKey(Employee::getName)) .collect(Collectors.toList()); System.out.println(姓名去重后的员工数量 distinctEmp.size());自定义去重工具方法通用可复用/** * Stream 对象属性去重通用工具方法 * param keyExtractor 去重依据的属性 */ public static T PredicateT distinctByKey(Function? super T, ? keyExtractor) { SetObject seen ConcurrentHashMap.newKeySet(); return t - seen.add(keyExtractor.apply(t)); }3.3 多属性联合去重需求根据部门姓名联合去重同部门同名员工视为重复数据ListEmployee multiDistinct getEmployeeList().stream() .filter(distinctByKey(emp - emp.getDept() _ emp.getName())) .collect(Collectors.toList());四、核心实战三Stream分组统计groupingBy分组是Stream最核心、最实用的高阶功能替代传统双层for循环分组支持单级分组、多级分组、分组后聚合统计对应SQL的group by语法。4.1 单级分组基础需求将员工集合按部门分组key部门名称value对应部门员工列表// 按部门单级分组 MapString, ListEmployee deptGroup getEmployeeList().stream() .collect(Collectors.groupingBy(Employee::getDept)); // 遍历分组结果 deptGroup.forEach((dept, empList) - { System.out.println(部门 dept 员工列表 empList.size() 人); empList.forEach(emp - System.out.println( emp.getName())); });4.2 分组后直接统计数量日常需求分组后统计每组数据条数无需遍历列表// 按部门分组统计每个部门人数 MapString, Long deptCount getEmployeeList().stream() .collect(Collectors.groupingBy( Employee::getDept, Collectors.counting() )); System.out.println(各部门人数统计 deptCount);4.3 多级分组高阶实战复杂业务先按部门分组再按性别分组双层分组// 多级分组部门 - 性别 - 员工列表 MapString, MapString, ListEmployee multiGroup getEmployeeList().stream() .collect(Collectors.groupingBy( Employee::getDept, Collectors.groupingBy(Employee::getGender) )); // 遍历多级分组结果 multiGroup.forEach((dept, genderMap) - { System.out.println(所属部门 dept); genderMap.forEach((gender, empList) - System.out.println( gender 员工 empList.size() 人) ); });4.4 分组后聚合求和/求平均值核心刚需分组后统计每组薪资总和、平均薪资// 按部门分组统计薪资总和、平均薪资 MapString, DoubleSummaryStatistics salaryStat getEmployeeList().stream() .collect(Collectors.groupingBy( Employee::getDept, Collectors.summarizingDouble(Employee::getSalary) )); // 遍历统计结果 salaryStat.forEach((dept, stat) - { System.out.println(部门 dept); System.out.println( 员工人数 stat.getCount()); System.out.println( 薪资总和 stat.getSum()); System.out.println( 平均薪资 stat.getAverage()); });五、核心实战四Stream数值求和、最值、统计Stream针对数值类型提供了专属聚合API无需手动遍历累加一行代码完成求和、最大值、最小值、平均值、总数统计。5.1 基础求和int/double// 方式1mapToDouble sum推荐简洁高效 double totalSalary getEmployeeList().stream() .mapToDouble(Employee::getSalary) .sum(); System.out.println(全体员工薪资总和 totalSalary); // 方式2reduce归约求和 Double reduceSum getEmployeeList().stream() .map(Employee::getSalary) .reduce(0.0, Double::sum); System.out.println(归约求和结果 reduceSum);5.2 条件求和过滤后求和需求统计研发部所有员工的薪资总和double deptSalarySum getEmployeeList().stream() .filter(emp - 研发部.equals(emp.getDept())) .mapToDouble(Employee::getSalary) .sum(); System.out.println(研发部薪资总和 deptSalarySum);5.3 最大值、最小值、平均值统计// 最高薪资 OptionalDouble maxSalary getEmployeeList().stream() .mapToDouble(Employee::getSalary) .max(); // 最低薪资 OptionalDouble minSalary getEmployeeList().stream() .mapToDouble(Employee::getSalary) .min(); // 平均薪资 OptionalDouble avgSalary getEmployeeList().stream() .mapToDouble(Employee::getSalary) .average(); System.out.println(最高薪资 maxSalary.getAsDouble()); System.out.println(最低薪资 minSalary.getAsDouble()); System.out.println(平均薪资 avgSalary.getAsDouble());5.4 全能统计工具summarizing一次性获取数量、总和、最值、平均值所有数据开发最常用DoubleSummaryStatistics stat getEmployeeList().stream() .mapToDouble(Employee::getSalary) .summaryStatistics(); System.out.println(总数 stat.getCount()); System.out.println(总和 stat.getSum()); System.out.println(最大值 stat.getMax()); System.out.println(最小值 stat.getMin()); System.out.println(平均值 stat.getAverage());六、高频组合实战真实业务场景一站式解决实际开发中不会单独使用某一个功能都是过滤去重分组求和组合使用下面给一个企业级完整案例。6.1 业务需求统计30岁以下、薪资大于8000、去重后的员工按部门分组统计各部门人数、薪资总和、平均薪资。6.2 完整Stream代码实现// 完整业务组合操作 MapString, DoubleSummaryStatistics businessResult getEmployeeList().stream() // 1. 过滤30岁以下 薪资大于8000 .filter(emp - emp.getAge() 30 emp.getSalary() 8000) // 2. 去重根据姓名去重 .filter(distinctByKey(Employee::getName)) // 3. 按部门分组 4. 聚合统计 .collect(Collectors.groupingBy( Employee::getDept, Collectors.summarizingDouble(Employee::getSalary) )); // 输出最终业务结果 businessResult.forEach((dept, stat) - { System.out.println( dept ); System.out.println(符合条件人数 stat.getCount()); System.out.println(薪资总合计 stat.getSum()); System.out.println(平均薪资 stat.getAverage()); });6.3 传统代码VS Stream代码总结传统写法需要循环遍历、临时集合存储、多层嵌套、手动统计代码50行极易出错Stream写法链式一行逻辑20行搞定逻辑清晰、零冗余、可读性拉满七、Stream开发避坑指南生产必看很多同学会用但用不好生产环境常见Bug全部整理到位7.1 流不可重复使用Stream流执行完终端操作后自动关闭再次调用会抛出IllegalStateException需要重新获取流。7.2 惰性求值陷阱中间操作不会执行只有终端操作执行后才会生效如果只写filter、map不写collect/forEach代码完全不执行。7.3 对象去重不要盲目用distinct()默认distinct基于地址/equals业务去重必须使用自定义属性去重工具方法。7.4 空集合空指针问题Stream操作空集合不会报错返回空结果无需手动判空比for循环更安全。7.5 并行流慎用parallelStream并行流效率高但非线程安全场景会出现数据错乱普通业务优先使用普通流。八、全文总结本文系统性讲解了JDK8 Stream流式编程的核心原理四大核心实战场景覆盖开发95%的集合操作需求过滤filter精准筛选有效数据替代if判断去重distinct基础类型对象属性去重解决重复数据问题分组groupingBy单级/多级分组实现数据分类统计聚合求和快速完成数值统计、最值、平均值计算Stream是Java开发的必备基本功熟练使用可以大幅简化代码、提升开发效率也是面试高频考点。建议大家收藏本文日常开发直接复制复用彻底告别冗余for循环下期预告JDK系列06HashMap底层源码详解扩容机制、红黑树转换、并发安全问题码字不易点赞收藏关注持续更新JDK底层、新特性、JVM调优、并发编程高质量干货