如何正确将流分组到自定义POJO?现有实现优化方案探讨
问题分析与优化方案
首先得指出你当前代码的核心问题:测试数据不符合你描述的业务场景,同时分组逻辑可以更高效简洁。
你提到实体的标量属性会重复出现,但你的测试代码里同一个id生成了不同的name(比如id=1对应name_a1和name_b1),而Department类的equals/hashCode是基于id和name生成的,这就导致同一个业务实体被当成了不同的分组键,自然没法合并。另外原实现分两次流处理,中间生成了冗余的集合,效率和可读性都有优化空间。
第一步:修正测试数据(符合业务场景)
先把测试数据改成同一实体的标量属性一致:
for(int i=1;i<=50;i++) { // 同一个id对应相同的name,符合"实体属性重复"的业务场景 summaries.add( new DepartmentSummaryRow(i, "name_"+i, "g1", 3 ) ); summaries.add( new DepartmentSummaryRow(i, "name_"+i, "g2", 9 ) ); }
优化方案1:用Collectors.toMap一步完成合并
直接通过toMap的合并器逻辑,在流处理中完成实体分组和统计Map的合并,不需要额外遍历:
private static void optimizedGrouping() { Gson g = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); List<DepartmentSummaryRow> summaries = new ArrayList<>(); // 修正后的测试数据 for(int i=1;i<=50;i++) { summaries.add( new DepartmentSummaryRow(i, "name_"+i, "g1", 3 ) ); summaries.add( new DepartmentSummaryRow(i, "name_"+i, "g2", 9 ) ); } Collection<Department> departments = summaries.stream() .collect(Collectors.toMap( // 分组键:用id+name构建Department(保证同一实体的键一致) row -> new Department(row.getId(), row.getName()), // 初始值:为当前行创建单个group的统计Map row -> { Map<String, Integer> map = new HashMap<>(); map.put(row.getGroupName(), row.getGroupMembersCount()); return map; }, // 合并器:把两个Map的统计值累加(如果有重复groupName则求和) (map1, map2) -> { map2.forEach((k, v) -> map1.merge(k, v, Integer::sum)); return map1; } )) .entrySet().stream() // 把合并后的统计Map绑定到Department实例 .map(entry -> { entry.getKey().getGroupCounts().putAll(entry.getValue()); return entry.getKey(); }) .collect(Collectors.toList()); System.out.println(g.toJson(departments)); }
优化方案2:自定义Collector(更优雅的并行友好实现)
如果需要支持并行流,或者想让逻辑更内聚,可以自定义Collector直接生成目标Department对象:
// 自定义Collector:从DepartmentSummaryRow生成Department private static Collector<DepartmentSummaryRow, ?, Department> departmentCollector() { return Collector.of( // 供应器:创建临时容器,存储Department和待合并的统计数据 () -> new AbstractMap.SimpleEntry<>(new Department(0, ""), new HashMap<String, Integer>()), // 累加器:处理每一行数据,初始化实体属性+累加统计 (entry, row) -> { Department dept = entry.getKey(); // 第一次处理该实体时初始化属性 if (dept.getId() == 0) { dept.setId(row.getId()); dept.setName(row.getName()); } // 合并group统计(重复groupName自动累加) entry.getValue().merge(row.getGroupName(), row.getGroupMembersCount(), Integer::sum); }, // 组合器:并行流时合并两个容器的统计数据 (entry1, entry2) -> { Department dept1 = entry1.getKey(); Department dept2 = entry2.getKey(); if (!dept1.equals(dept2)) { throw new IllegalArgumentException("无法合并不同部门的数据"); } entry2.getValue().forEach((k, v) -> entry1.getValue().merge(k, v, Integer::sum)); return entry1; }, // 终结者:把统计数据写入Department的groupCounts entry -> { entry.getKey().getGroupCounts().putAll(entry.getValue()); return entry.getKey(); } ); } // 使用自定义Collector的分组逻辑 private static void customCollectorGrouping() { Gson g = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); List<DepartmentSummaryRow> summaries = new ArrayList<>(); for(int i=1;i<=50;i++) { summaries.add( new DepartmentSummaryRow(i, "name_"+i, "g1", 3 ) ); summaries.add( new DepartmentSummaryRow(i, "name_"+i, "g2", 9 ) ); } Collection<Department> departments = summaries.stream() .collect(Collectors.groupingBy( row -> new Department(row.getId(), row.getName()), LinkedHashMap::new, // 保留插入顺序 departmentCollector() )) .values(); System.out.println(g.toJson(departments)); }
关键注意事项
- 实体
equals/hashCode的正确性:你的Department类已经用@EqualsAndHashCode.Exclude排除了groupCounts,这部分是对的,确保分组只依赖实体的核心标量属性。 - 输入数据的一致性:必须保证同一实体的所有行,其标量属性(
id、name)完全一致,如果是SQL查询导致的属性不一致,需要调整SQL(比如用DISTINCT或者确保关联时不会重复覆盖实体属性)。 - 性能优化:优化后的方案避免了中间
List的创建和二次遍历,在数据量大的场景下会有明显的性能提升。
内容的提问来源于stack exchange,提问作者Teddy
相关产品推荐
相关产品推荐

