ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Stream API 实战:分组去重与最大值筛选

Java Stream API 实战:分组去重与最大值筛选 Java Stream API 实战分组去重与最大值筛选一次 Stream 链式调用的完整拆解前言在日常开发中我们经常遇到这样的需求对列表数据按某些字段去重并在重复项中保留满足特定条件的记录。比如按产品编码批次号去重保留priorityLevel最高的那条。本文将通过一段真实的 Stream 代码带你一步步拆解其执行过程彻底理解每一步的输入、输出和含义。问题场景假设我们有一个ProductOrder对象列表包含以下字段字段说明productCode产品编码batchNo批次号priorityLevel优先级Integer可为 null数值越大优先级越高业务需求按productCode batchNo组合去重如果有多条相同组合的记录只保留priorityLevel最大的那条如果priorityLevel为null视为最小值PO 类定义publicclassProductOrder{privateStringproductCode;// 产品编码privateStringbatchNo;// 批次号privateIntegerpriorityLevel;// 优先级越大越优先privateStringorderId;// 订单IDprivateIntegerquantity;// 数量// ... 其他字段// getter/setter 省略}完整代码productOrderListproductOrderList.stream().collect(Collectors.groupingBy(order-order.getProductCode()_order.getBatchNo())).values().stream().map(group-group.stream().max(Comparator.comparingInt(order-Objects.isNull(order.getPriorityLevel())?Integer.MIN_VALUE:order.getPriorityLevel()))).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());逐步拆解第 1 步转流productOrderList.stream()返回类型StreamProductOrder将原始 List 转换为 Stream为后续链式操作做准备。第 2 步分组.collect(Collectors.groupingBy(order-order.getProductCode()_order.getBatchNo()))返回类型MapString, ListProductOrder作用按产品编码_批次号作为 Key 进行分组相同 Key 的记录归入同一个 List。示例数据KeyValue (List)“P001_B001”[orderA, orderB]“P001_B002”[orderC]“P002_B001”[orderD, orderE, orderF]第 3 步获取所有分组值.values()返回类型CollectionListProductOrder作用取出 Map 中所有的 Value 集合即每个分组的 List丢弃 Key。示例[[orderA,orderB],[orderC],[orderD,orderE,orderF]]第 4 步再次转流.stream()返回类型StreamListProductOrder作用将 Collection 转为 Stream此时流中的每个元素就是一个分组 List。第 5 步每组选最大值.map(group-group.stream().max(Comparator.comparingInt(order-Objects.isNull(order.getPriorityLevel())?Integer.MIN_VALUE:order.getPriorityLevel())))返回类型StreamOptionalProductOrder作用对每个分组 List 进行处理找出其中priorityLevel最大的元素。比较器逻辑如果priorityLevel为null返回Integer.MIN_VALUE视为最小否则返回实际数值处理示例分组元素及优先级最大值结果[orderA(5), orderB(3)]5, 35Optional[orderA][orderC(null)]null最小值Optional[orderC][orderD(8), orderE(10), orderF(6)]8, 10, 610Optional[orderE]第 6 步过滤空 Optional.filter(Optional::isPresent)返回类型StreamOptionalProductOrder作用过滤掉空的 Optional。虽然本例中分组非空不会过滤任何元素但这是防御性编程的好习惯。第 7 步解包 Optional.map(Optional::get)返回类型StreamProductOrder作用将 Optional 解包取出真正的ProductOrder对象。第 8 步收集结果.collect(Collectors.toList());返回类型ListProductOrder作用将 Stream 收集为 List得到最终结果。完整数据流演示假设原始数据如下序号productCodebatchNopriorityLevelorderId1P001B0015ORD0012P001B0013ORD0023P001B002nullORD0034P002B0018ORD0045P002B00110ORD0056P002B0016ORD006执行流程原始数据 (6条) ↓ 按 产品编码批次号 分组 ↓ ┌──────────────────────────────────────────────────────────────┐ │ 分组1: P001_B001 → [ORD001(5), ORD002(3)] │ │ 分组2: P001_B002 → [ORD003(null)] │ │ 分组3: P002_B001 → [ORD004(8), ORD005(10), ORD006(6)] │ └──────────────────────────────────────────────────────────────┘ ↓ 每组选 priorityLevel 最大的 ↓ ┌──────────────────────────────────────────────────────────────┐ │ 分组1: 选 ORD001 (priorityLevel5) │ │ 分组2: 选 ORD003 (priorityLevelnull, 唯一) │ │ 分组3: 选 ORD005 (priorityLevel10) │ └──────────────────────────────────────────────────────────────┘ ↓ 最终结果 (3条) ↓ [ORD001, ORD003, ORD005]关键点总结阶段操作输入输出元素数量变化1stream()ListStream6 → 62groupingBy()StreamMapString, List6 → 3 个分组3values()MapCollection3 个分组4stream()CollectionStream3 个分组5map(max())每个 ListOptional3 → 36filter(isPresent)Optional 流Optional 流3 → 37map(get)Optional 流Stream3 → 38collect(toList())StreamList3 → 3最终效果6 条数据 → 3 条数据每个组合键保留 1 条潜在问题与改进建议1. 相同最大值时的不确定性如果同一组内有多条记录的priorityLevel相同且最大max()只返回其中一条不保证是哪一个。建议如需稳定结果可在比较器中添加次要排序条件。Comparator.comparingInt(order-Objects.isNull(order.getPriorityLevel())?Integer.MIN_VALUE:order.getPriorityLevel()).thenComparing(ProductOrder::getOrderId)// 添加次要排序2. Key 拼接的分隔符问题使用_拼接字段如果字段本身包含_可能导致 Key 冲突。建议使用不会出现在字段中的分隔符或构建复合 Key 对象// 方案1使用特殊分隔符order-order.getProductCode()|||order.getBatchNo()// 方案2使用 Pair 或自定义 Key 类order-newAbstractMap.SimpleEntry(order.getProductCode(),order.getBatchNo())3. 可读性优化长链式调用可读性较差建议适当换行和注释productOrderListproductOrderList.stream()// 1. 按产品编码批次号分组.collect(Collectors.groupingBy(order-order.getProductCode()_order.getBatchNo())).values().stream()// 2. 每组保留 priorityLevel 最大的记录.map(group-group.stream().max(Comparator.comparingInt(order-Objects.isNull(order.getPriorityLevel())?Integer.MIN_VALUE:order.getPriorityLevel()))).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());4. 替代方案使用toMap合并也可以使用Collectors.toMap()实现相同功能代码更简洁productOrderListnewArrayList(productOrderList.stream().collect(Collectors.toMap(order-order.getProductCode()_order.getBatchNo(),Function.identity(),(existing,replacement)-{IntegerexistValexisting.getPriorityLevel()null?Integer.MIN_VALUE:existing.getPriorityLevel();IntegerreplaceValreplacement.getPriorityLevel()null?Integer.MIN_VALUE:replacement.getPriorityLevel();returnexistValreplaceVal?existing:replacement;})).values());5. 空值处理优化如果priorityLevel为null的情况较多可以考虑使用Comparator.nullsFirst()或Comparator.nullsLast().map(group-group.stream().max(Comparator.comparing(ProductOrder::getPriorityLevel,Comparator.nullsFirst(Comparator.naturalOrder()))))性能考虑数据量时间复杂度空间复杂度N 条记录O(N)O(N)groupingBy需要遍历一次建立 Mapmax()对每个分组遍历一次总体时间复杂度为 O(N)适合大多数场景结语Stream API 的强大之处在于用声明式的方式处理集合操作。通过这篇文章你应该能够✅ 理解groupingByvaluesmax的组合用法✅ 掌握 Stream 链式调用中每一步的输入输出✅ 学会处理null值的比较逻辑✅ 了解该方案的优缺点及替代方案在实际项目中根据团队编码规范和可读性要求选择最适合的实现方式即可。完整示例代码importjava.util.*;importjava.util.stream.Collectors;publicclassStreamDemo{publicstaticvoidmain(String[]args){// 1. 准备数据ListProductOrderproductOrderListArrays.asList(newProductOrder(P001,B001,5,ORD001),newProductOrder(P001,B001,3,ORD002),newProductOrder(P001,B002,null,ORD003),newProductOrder(P002,B001,8,ORD004),newProductOrder(P002,B001,10,ORD005),newProductOrder(P002,B001,6,ORD006));// 2. 执行去重逻辑ListProductOrderresultproductOrderList.stream().collect(Collectors.groupingBy(order-order.getProductCode()_order.getBatchNo())).values().stream().map(group-group.stream().max(Comparator.comparingInt(order-Objects.isNull(order.getPriorityLevel())?Integer.MIN_VALUE:order.getPriorityLevel()))).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());// 3. 输出结果result.forEach(order-System.out.println(order.getOrderId() | order.getProductCode()_order.getBatchNo() | priorityorder.getPriorityLevel()));}}classProductOrder{privateStringproductCode;privateStringbatchNo;privateIntegerpriorityLevel;privateStringorderId;publicProductOrder(StringproductCode,StringbatchNo,IntegerpriorityLevel,StringorderId){this.productCodeproductCode;this.batchNobatchNo;this.priorityLevelpriorityLevel;this.orderIdorderId;}// getter/setter 省略}输出结果ORD001 | P001_B001 | priority5 ORD003 | P001_B002 | prioritynull ORD005 | P002_B001 | priority10如果觉得有帮助欢迎点赞、收藏、转发
返回列表