
本文详解如何使用 java stream api 按指定属性(如城市)对对象列表分组,并高效提取每组第一个对象,生成唯一键值对应的精简列表。
本文详解如何使用 java stream api 按指定属性(如城市)对对象列表分组,并高效提取每组第一个对象,生成唯一键值对应的精简列表。
在实际开发中,我们常需从一组对象中“按某字段去重”,但不同于 distinct() 的全对象比较,业务场景往往要求:保留每个分组(如相同 city)中的任意一个代表项(通常为首个)。Java 8+ Stream 并未内置 distinctByKey 方法,但可通过组合 Collectors.groupingBy 与后续映射优雅实现。
✅ 推荐方案:groupingBy + map().get(0)
核心思路是先按目标字段(如 Person::getCity)分组,再从每个分组的 List
import java.util.*;
import java.util.stream.Collectors;
List<Person> people = Arrays.asList(
new Person("New York", "foo", "bar"),
new Person("New York", "bar", "foo"),
new Person("New Jersey", "foo", "bar"),
new Person("New Jersey", "bar", "foo")
);
List<Person> firstByCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity))
.values() // → Collection<List<Person>>
.stream()
.map(list -> list.get(0)) // 取每组第一个
.collect(Collectors.toList());
System.out.println(firstByCity);
// 输出: [{ city: New York, firstName: foo, lastName: bar },
// { city: New Jersey, firstName: foo, lastName: bar }]? 提示:Collectors.groupingBy(Person::getCity) 返回 Map
>,其 values() 是各城市对应人员列表的集合,流式遍历后取 get(0) 即得每城一人。
⚙️ 进阶封装:通用分组工具方法
为提升复用性与可读性,可封装为静态泛型工具方法(支持自定义键与值映射):
立即学习“Java免费学习笔记(深入)”;
public static <E, K> Map<K, List<E>> groupBy(
Collection<E> collection, Function<E, K> keyFn) {
return collection.stream()
.collect(Collectors.groupingBy(keyFn));
}
// 使用示例
List<Person> firstByCity = groupBy(people, Person::getCity)
.values().stream()
.map(list -> list.get(0))
.collect(Collectors.toList());若还需转换值(如只取姓名),可扩展为双函数版本(见原答案中的 groupBy(..., keyFn, valueFn)),但本场景中直接操作原始对象已足够。
⚠️ 注意事项与最佳实践
-
空安全:确保 people 非 null;若分组后某 List
可能为空(极罕见),建议用 list.stream().findFirst().orElse(null) 替代 list.get(0) 避免 IndexOutOfBoundsException。 - 顺序保证:groupingBy 默认使用 HashMap,不保证分组顺序;若需按原始顺序输出(如先出现的城市优先),应改用 Collectors.groupingBy(keyFn, LinkedHashMap::new, Collectors.toList())。
- 性能考量:该方案时间复杂度为 O(n),空间复杂度为 O(k),其中 k 为唯一键数量,适用于大多数中等规模数据集;超大数据量时可考虑 TreeSet 自定义比较器或并行流(注意线程安全)。
-
替代方案对比:
- ❌ distinct() 仅支持全对象 equals(),无法按字段去重;
- ⚠️ Collectors.toMap() 虽可实现(toMap(Person::getCity, Function.identity(), (a,b)->a)),但返回 Map 需额外 .values().stream().collect(...),且键冲突策略需显式处理,可读性略低。
✅ 总结
使用 Collectors.groupingBy(keyFn) 分组后取每组首元素,是 Java Stream 中实现“按字段保留首个对象”的标准、清晰且高效的方式。它语义明确、易于理解与维护,适合作为通用工具模式集成到项目工具类中。掌握此模式,可快速应对各类“去重取样”需求,如按部门取负责人、按类别取默认商品等。










