
本文介绍如何使用 Java Stream API(支持 Java 11+)对 Candidate 对象列表按 personId 分组,为每个分组选取 updateDate 最新的主记录,并将其余同组 codeHswCandId 收集至 codeHswCandIdRelated 字段中。
本文介绍如何使用 java stream api(支持 java 11+)对 `candidate` 对象列表按 `personid` 分组,为每个分组选取 `updatedate` 最新的主记录,并将其余同组 `codehswcandid` 收集至 `codehswcandidrelated` 字段中。
在实际业务开发中,常需将一批具有相同标识(如 personId)的实体,按时间戳(如 updateDate)选出最新主项,并将其他项归类为“关联项”。本教程以 Candidate 类为例,演示如何高效、可读地完成该任务。
核心思路
- 分组依据:personId
- 主记录选取规则:每组中 updateDate 最大的那条记录(即最近更新者)
- 关联字段填充:将同组内其余 codeHswCandId 收集到 codeHswCandIdRelated 列表中(排除主记录自身的 codeHswCandId)
✅ 推荐方案(Java 12+):使用 Collectors.teeing
teeing 是 Java 12 引入的高级收集器,支持单次遍历、并行构建两个中间结果,语义清晰且性能更优:
List<Candidate> result = candidateList.stream()
.collect(Collectors.teeing(
// 分支一:按 personId 聚合,保留 updateDate 最大的 Candidate
Collectors.toMap(
Candidate::getPersonId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Candidate::getUpdateDate))
),
// 分支二:按 personId 分组,收集所有 codeHswCandId 到 List<String>
Collectors.groupingBy(
Candidate::getPersonId,
Collectors.mapping(Candidate::getCodeHswCandId, Collectors.toList())
),
// 合并逻辑:遍历主记录 Map,构造新 Candidate
(mainMap, relatedMap) -> mainMap.entrySet().stream()
.map(entry -> {
Integer pid = entry.getKey();
Candidate main = entry.getValue();
List<String> allCodes = relatedMap.getOrDefault(pid, List.of());
List<String> related = allCodes.stream()
.filter(code -> !code.equals(main.getCodeHswCandId()))
.collect(Collectors.toList());
return Candidate.builder()
.personId(pid)
.codeHswCandId(main.getCodeHswCandId())
.codeHswCandIdRelated(related)
.build();
})
.collect(Collectors.toList())
));⚙️ 兼容方案(Java 11 及以下):两次流式处理
若项目受限于 Java 11,可采用两次独立流操作构建中间映射,逻辑同样清晰:
// Step 1: 构建 personId → 最新 Candidate 的映射
Map<Integer, Candidate> mainCandidates = candidateList.stream()
.collect(Collectors.toMap(
Candidate::getPersonId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Candidate::getUpdateDate))
));
// Step 2: 构建 personId → 所有 codeHswCandId 列表的映射
Map<Integer, List<String>> relatedCodes = candidateList.stream()
.collect(Collectors.groupingBy(
Candidate::getPersonId,
Collectors.mapping(Candidate::getCodeHswCandId, Collectors.toList())
));
// Step 3: 合并生成最终结果
List<Candidate> result = mainCandidates.entrySet().stream()
.map(entry -> {
Integer pid = entry.getKey();
Candidate main = entry.getValue();
List<String> all = relatedCodes.getOrDefault(pid, List.of());
List<String> related = all.stream()
.filter(code -> !code.equals(main.getCodeHswCandId()))
.toList(); // Java 16+;若用低版本,替换为 .collect(Collectors.toList())
return Candidate.builder()
.personId(pid)
.codeHswCandId(main.getCodeHswCandId())
.codeHswCandIdRelated(related)
.build();
})
.collect(Collectors.toList());⚠️ 注意事项与最佳实践
- 空值安全:getOrDefault(pid, List.of()) 防止 relatedCodes.get(pid) 返回 null 导致 NPE。
- 不可变性:List.of() 返回不可修改列表,若后续需修改 codeHswCandIdRelated,请显式使用 new ArrayList(...)。
- 性能考量:teeing 方案仅遍历一次源集合,内存与 CPU 更友好;双流方案虽直观,但需两次遍历(O(2n)),大数据量时建议优先选用 teeing。
- Builder 链式调用:确保 Lombok @Builder 已正确配置(无参数构造器、字段可见性等),否则编译可能失败。
- 时间比较可靠性:LocalDateTime 的 compareTo() 天然支持毫秒级精确比较,无需额外格式化或转换。
✅ 验证输出
对题设输入数据执行上述任一方案后,result 将严格等于:
立即学习“Java免费学习笔记(深入)”;
List.of(
Candidate.builder()
.personId(1)
.codeHswCandId("1")
.codeHswCandIdRelated(List.of("2", "3"))
.build(),
Candidate.builder()
.personId(2)
.codeHswCandId("4")
.codeHswCandIdRelated(List.of("5", "6"))
.build()
);通过本教程,你已掌握一种兼具表达力、健壮性与兼容性的分组聚合模式。该模式可轻松迁移至其他类似场景(如按 userId 分组选最新订单、按 orderId 归集子项 SKU 等),是现代 Java 函数式编程的典型实践。










