
本文介绍在 Java 中利用 Stream API 实现“按指定属性分组并各取一个代表对象”的典型需求,重点讲解基于 Collectors.groupingBy 的分组 + 映射提取方案,并提供可复用的通用工具方法。
本文介绍在 java 中利用 stream api 实现“按指定属性分组并各取一个代表对象”的典型需求,重点讲解基于 `collectors.groupingby` 的分组 + 映射提取方案,并提供可复用的通用工具方法。
在实际开发中,常需从集合中按某字段(如 city)分组,并为每组保留一个代表性元素——例如“每个城市只取第一位人员”。Java 8+ 的 Stream API 本身不直接支持“按属性去重”,但可通过组合 groupingBy 与后续映射高效实现。
核心思路是:
- 使用 Collectors.groupingBy(keyMapper) 将列表按目标属性(如 Person::getCity)分组为 Map
>; - 遍历分组结果的 entrySet(),对每个 List
取索引 0 元素(即首项); - 收集为新 List
。
以下为完整可运行示例:
import java.util.*;
import java.util.stream.Collectors;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
public class StreamDistinctByGroup {
public static void main(String[] args) {
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")
);
// ✅ 按 city 分组,每组取第一个 Person
List<Person> firstByCity = groupBy(people, Person::getCity)
.values().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 }]
}
// ✅ 通用分组工具方法(支持自定义 key 和 value 提取)
public static <E, K, V> Map<K, List<V>> groupBy(
Collection<E> collection,
Function<E, K> keyFn,
Function<E, V> valueFn) {
return collection.stream()
.map(item -> new SimpleEntry<>(keyFn.apply(item), valueFn.apply(item)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())
));
}
// ✅ 简化版:仅按 key 分组,value 为原对象
public static <E, K> Map<K, List<E>> groupBy(Collection<E> collection, Function<E, K> keyFn) {
return groupBy(collection, keyFn, Function.identity());
}
public static class Person {
private final String city;
private final String firstName;
private final String lastName;
public Person(String city, String firstName, String lastName) {
this.city = city;
this.firstName = firstName;
this.lastName = lastName;
}
public String getCity() { return city; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
@Override
public String toString() {
return String.format("{ city: %s, firstName: %s, lastName: %s }",
city, firstName, lastName);
}
}
}⚠️ 注意事项:
立即学习“Java免费学习笔记(深入)”;
- list.get(0) 前请确保 list 非空(groupingBy 保证每个值列表至少含 1 个元素,故安全);
- 若需按特定规则选代表(如按姓名排序后取最小),可将 list.get(0) 替换为 list.stream().sorted(...).findFirst().orElse(null);
- 该方案时间复杂度为 O(n),优于嵌套循环,且语义清晰、易于维护;
- 不建议滥用 distinct() —— 它依赖 equals/hashCode 全字段比较,无法实现“按单属性去重”。
✅ 总结:groupBy + values().stream().map(...) 是处理“每组取一”场景最直观、健壮且符合函数式编程思想的 Stream 解法。配合泛型工具方法,可轻松复用于任意类型与任意分组键,显著提升代码复用性与可读性。










