
本文详解如何使用Java Stream API(支持Java 11+)对候选对象按personId分组,提取每个分组中updateDate最新的主记录,并将其余codeHswCandId归入codeHswCandIdRelated列表。
本文详解如何使用java stream api(支持java 11+)对候选对象按`personid`分组,提取每个分组中`updatedate`最新的主记录,并将其余`codehswcandid`归入`codehswcandidrelated`列表。
在实际业务开发中,常需将一批同质对象按某个标识(如personId)聚合,并基于时间戳等字段选出“主代表”,再将其余成员信息结构化地挂载为关联字段——例如本例中,每个Candidate需保留其最新更新的codeHswCandId作为主ID,其余同personId的codeHswCandId则统一收纳进codeHswCandIdRelated列表。
该需求本质是分组 + 主记录选取 + 关联数据提取三步逻辑的组合。Java 12 引入的 Collectors.teeing 提供了优雅的单次遍历解决方案;而 Java 11 及以下版本则可通过两次流式处理实现等效效果,兼顾可读性与性能。
✅ 推荐方案(Java 12+):使用 Collectors.teeing
teeing 支持并行收集两个独立的中间结果(如主记录Map和ID列表Map),再通过合并函数构造最终对象。代码简洁、语义清晰,且仅遍历原始列表一次:
List<Candidate> result = candidateList.stream()
.collect(Collectors.teeing(
// 分支1:按 personId 分组,取 updateDate 最大的 Candidate
Collectors.toMap(
Candidate::getPersonId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Candidate::getUpdateDate))
),
// 分支2:按 personId 分组,收集所有 codeHswCandId 到 List<String>
Collectors.groupingBy(
Candidate::getPersonId,
Collectors.mapping(Candidate::getCodeHswCandId, Collectors.toList())
),
// 合并函数:遍历主记录Map,构建新Candidate
(mainMap, idListMap) -> mainMap.entrySet().stream()
.map(entry -> {
Integer pid = entry.getKey();
Candidate main = entry.getValue();
List<String> allIds = idListMap.getOrDefault(pid, List.of());
// 过滤掉主记录自身的 codeHswCandId
List<String> related = allIds.stream()
.filter(id -> !id.equals(main.getCodeHswCandId()))
.collect(Collectors.toList());
return Candidate.builder()
.personId(pid)
.codeHswCandId(main.getCodeHswCandId())
.codeHswCandIdRelated(related)
.build();
})
.collect(Collectors.toList())
));⚙️ 兼容方案(Java 11 及以下):两次流处理
若项目受限于 JDK 版本,可拆分为两步:先构建主记录映射,再构建ID列表映射,最后合并。虽多一次遍历,但逻辑更直观,调试友好:
立即学习“Java免费学习笔记(深入)”;
// 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>> idGroups = 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> allIds = idGroups.getOrDefault(pid, List.of());
List<String> related = allIds.stream()
.filter(id -> !id.equals(main.getCodeHswCandId()))
.collect(Collectors.toList());
return Candidate.builder()
.personId(pid)
.codeHswCandId(main.getCodeHswCandId())
.codeHswCandIdRelated(related)
.build();
})
.collect(Collectors.toList());⚠️ 注意事项与最佳实践
- 空值防护:getOrDefault(pid, List.of()) 避免 NullPointerException;若原始数据中存在 null 的 codeHswCandId,建议在映射前过滤(如 .filter(Objects::nonNull))。
- 时间精度一致性:LocalDateTime 比较依赖纳秒级精度,确保数据中无因时区或格式化导致的意外偏差。
- 性能考量:对于超大数据集(>10⁵ 条),两次流处理的内存与CPU开销略高于 teeing,但差异通常可忽略;若追求极致性能,可考虑预排序后手动分组。
- Builder 安全性:Lombok @Builder 默认允许部分字段为空,此处所有字段均显式赋值,无需额外校验。
通过上述任一方式,输入的6个候选人即可精准转换为目标结构:每个personId仅保留一条最新记录作为主体,其余ID自动归入关联列表,满足典型主从聚合建模需求。










