Java List 转 Map 的几种常用方式
Java List 转 Map
Java List 转 Map 的几种常用方式
适用范围:Java 8+ Stream API
核心类:java.util.stream.Collectors
重点方法:Collectors.toMap()、Collectors.groupingBy()、Collectors.mapping()、Collectors.toConcurrentMap()
API 依据:Oracle Java 官方Collectors文档。
1. 基础示例对象#
本文示例使用如下 User 对象:
javapublic class User { private Long id; private String name; private Integer age; private String department; public User(Long id, String name, Integer age, String department) { this.id = id; this.name = name; this.age = age; this.department = department; } public Long getId() { return id; } public String getName() { return name; } public Integer getAge() { return age; } public String getDepartment() { return department; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", department='" + department + '\'' + '}'; } }
测试数据:
javaList userList = List.of( new User(1L, "Alice", 18, "GIS"), new User(2L, "Bob", 20, "AI"), new User(3L, "Cindy", 22, "GIS"), new User(1L, "Alice-New", 19, "RemoteSensing") );
2. 一对一:id -> User#
2.1 没有重复 key 的情况#
javaMap userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity() ));
含义:
javaCollectors.toMap(keyMapper, valueMapper)
User::getId:生成 Map 的 key。Function.identity():value 就是当前对象本身。
注意:如果 key 重复,这种写法会在收集时抛出 IllegalStateException。如果 key 可能重复,应使用带 mergeFunction 的重载方法。
2.2 有重复 key:保留后一个对象#
javaMap userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));
含义:
oldValue:旧值。newValue:新值。- 返回
newValue,表示后面的值覆盖前面的值。
这比写成 (key1, key2) -> key2 更准确,因为这两个参数不是 key,而是发生 key 冲突时的两个 value。
2.3 有重复 key:保留第一个对象#
javaMap userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> oldValue ));
含义:
- 如果 id 重复,保留第一次出现的 User。
3. 一对一:id -> 某个字段#
3.1 id -> name#
javaMap idNameMap = userList.stream() .collect(Collectors.toMap( User::getId, User::getName, (oldValue, newValue) -> newValue ));
结果类似:
java{ 1=Alice-New, 2=Bob, 3=Cindy }
3.2 id -> age#
javaMap idAgeMap = userList.stream() .collect(Collectors.toMap( User::getId, User::getAge, (oldValue, newValue) -> newValue ));
4. 指定 Map 类型#
默认 toMap() 返回的 Map 类型不应被业务代码强依赖。官方文档说明返回 Map 的具体类型、可变性、序列化能力和线程安全性都不保证。需要指定 Map 类型时,应使用带 mapFactory 的重载方法。
4.1 转成 LinkedHashMap:保持插入顺序#
javaMap userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue, LinkedHashMap::new ));
适用场景:
- 前端展示需要保持原列表顺序。
- 导出报表。
- 菜单、字典、树结构预处理。
4.2 转成 TreeMap:按 key 排序#
javaMap userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue, TreeMap::new ));
适用场景:
- 按 id 排序。
- 按时间戳排序。
- 按字典 key 排序。
5. 一对多:一个 key 对应多个对象#
5.1 按 department 分组#
javaMap> groupByDepartment = userList.stream() .collect(Collectors.groupingBy(User::getDepartment));
结果类似:
java{ GIS=[Alice, Cindy], AI=[Bob], RemoteSensing=[Alice-New] }
5.2 按 id 分组:id -> List#
javaMap> groupById = userList.stream() .collect(Collectors.groupingBy(User::getId));
适用场景:
- 同一个 key 对应多条记录。
- 一个订单对应多个明细。
- 一个分类对应多个内容。
- 一个用户对应多条操作记录。
6. 一对多后,只保留某个字段#
6.1 department -> List#
javaMap> departmentNames = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.mapping(User::getName, Collectors.toList()) ));
6.2 department -> List#
javaMap> departmentIds = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.mapping(User::getId, Collectors.toList()) ));
7. 分组后做统计#
7.1 department -> 用户数量#
javaMap departmentCount = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.counting() ));
7.2 department -> 平均年龄#
javaMap departmentAvgAge = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.averagingInt(User::getAge) ));
8. 分组后取最大值 / 最小值#
8.1 每个部门年龄最大的用户#
javaMap> maxAgeUserByDepartment = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.maxBy(Comparator.comparing(User::getAge)) ));
返回 Optional 的原因是:下游收集器 maxBy 的结果本身就是 Optional。
8.2 如果确定每组一定有值#
javaMap maxAgeUserByDepartment = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.collectingAndThen( Collectors.maxBy(Comparator.comparing(User::getAge)), Optional::get ) ));
注意:这种写法默认每组一定有值,否则 Optional::get 会有风险。
9. 并发 Map#
9.1 使用 toConcurrentMap#
javaConcurrentMap concurrentMap = userList.parallelStream() .collect(Collectors.toConcurrentMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));
适用场景:
parallelStream()。- 并发收集。
- 结果需要
ConcurrentMap。
9.2 使用 groupingByConcurrent#
javaConcurrentMap> concurrentGroup = userList.parallelStream() .collect(Collectors.groupingByConcurrent(User::getDepartment));
10. null 处理注意事项#
10.1 key 可能为 null#
虽然部分 Map 实现允许 null key,但业务代码不建议依赖 null key。推荐先过滤:
javaMap userMap = userList.stream() .filter(user -> user.getId() != null) .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));
10.2 value 可能为 null#
Collectors.toMap() 中 value 为 null 时容易触发 NullPointerException。推荐提前处理:
javaMap idNameMap = userList.stream() .filter(user -> user.getId() != null) .collect(Collectors.toMap( User::getId, user -> Optional.ofNullable(user.getName()).orElse(""), (oldValue, newValue) -> newValue ));
11. 常见写法汇总#
11.1 List -> Map<id, Object>#
javaMap map = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));
11.2 List -> Map<id, field>#
javaMap map = userList.stream() .collect(Collectors.toMap( User::getId, User::getName, (oldValue, newValue) -> newValue ));
11.3 List -> Map<key, List>#
javaMap> map = userList.stream() .collect(Collectors.groupingBy(User::getDepartment));
11.4 List -> Map<key, List>#
javaMap> map = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.mapping(User::getName, Collectors.toList()) ));
11.5 List -> LinkedHashMap#
javaMap map = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue, LinkedHashMap::new ));
12. toMap 和 groupingBy 的选择#
| 场景 | 推荐方法 |
|---|---|
| 一个 key 对应一个 value | Collectors.toMap() |
| 一个 key 可能对应多个 value | Collectors.groupingBy() |
| key 重复但只保留一个 | toMap(..., mergeFunction) |
| key 重复并全部保留 | groupingBy() |
| 需要指定 Map 类型 | toMap(..., mapFactory) |
| 需要并发 Map | toConcurrentMap() / groupingByConcurrent() |
| 分组后只要某个字段 | groupingBy(..., mapping(...)) |
| 分组后统计数量 | groupingBy(..., counting()) |
13. 常见错误#
13.1 key 重复导致异常#
错误写法:
javaMap map = userList.stream() .collect(Collectors.toMap(User::getId, Function.identity()));
当 id 重复时,会抛出异常。
正确写法:
javaMap map = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));
13.2 误以为 key1/key2 是 key#
很多示例会写:
java(key1, key2) -> key2
实际上它们不是 key,而是发生 key 冲突时的两个 value。
更推荐命名为:
java(oldValue, newValue) -> newValue
14. 推荐实践#
推荐 1:只要 key 可能重复,就显式写 mergeFunction#
java.collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));
推荐 2:变量名使用 oldValue / newValue#
java(oldValue, newValue) -> newValue
推荐 3:需要顺序时使用 LinkedHashMap#
java.collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue, LinkedHashMap::new ));
推荐 4:一对多优先使用 groupingBy#
java.collect(Collectors.groupingBy(User::getDepartment));
15. 官方 API 依据#
本文基于 Java 官方 Collectors API 整理,重点参考:
Collectors.toMap(...)Collectors.groupingBy(...)Collectors.toConcurrentMap(...)Collectors.groupingByConcurrent(...)Collectors.mapping(...)Function.identity()
官方 API 要点:
toMap(keyMapper, valueMapper)用于将元素收集为 Map。- 如果 key 重复,二参数
toMap会抛出异常。 - 如果 key 可能重复,应使用带
mergeFunction的重载。 - 如果要指定 Map 类型,应使用带
mapFactory的重载。 groupingBy(classifier)用于一对多分组。groupingByConcurrent和toConcurrentMap用于并发 Map 场景。
16. 结论#
Java List 转 Map 最常见有三类:
- 一对一:
Collectors.toMap() - 一对多:
Collectors.groupingBy() - 分组后转换 / 统计:
groupingBy + mapping/counting/maxBy
实际开发中最重要的是:
- 明确 key 是否会重复。
- 重复时必须定义 merge 策略。
- 一对多不要强行用
toMap。 - 有顺序要求时指定
LinkedHashMap。 - 有并发要求时使用 concurrent collector。