博客

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 对象:

java
public 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 + '\'' + '}'; } }

测试数据:

java
List 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 的情况#

java
Map userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity() ));

含义:

java
Collectors.toMap(keyMapper, valueMapper)
  • User::getId:生成 Map 的 key。
  • Function.identity():value 就是当前对象本身。

注意:如果 key 重复,这种写法会在收集时抛出 IllegalStateException。如果 key 可能重复,应使用带 mergeFunction 的重载方法。


2.2 有重复 key:保留后一个对象#

java
Map 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:保留第一个对象#

java
Map userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> oldValue ));

含义:

  • 如果 id 重复,保留第一次出现的 User。

3. 一对一:id -> 某个字段#

3.1 id -> name#

java
Map 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#

java
Map idAgeMap = userList.stream() .collect(Collectors.toMap( User::getId, User::getAge, (oldValue, newValue) -> newValue ));

4. 指定 Map 类型#

默认 toMap() 返回的 Map 类型不应被业务代码强依赖。官方文档说明返回 Map 的具体类型、可变性、序列化能力和线程安全性都不保证。需要指定 Map 类型时,应使用带 mapFactory 的重载方法。

4.1 转成 LinkedHashMap:保持插入顺序#

java
Map userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue, LinkedHashMap::new ));

适用场景:

  • 前端展示需要保持原列表顺序。
  • 导出报表。
  • 菜单、字典、树结构预处理。

4.2 转成 TreeMap:按 key 排序#

java
Map userMap = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue, TreeMap::new ));

适用场景:

  • 按 id 排序。
  • 按时间戳排序。
  • 按字典 key 排序。

5. 一对多:一个 key 对应多个对象#

5.1 按 department 分组#

java
Map> groupByDepartment = userList.stream() .collect(Collectors.groupingBy(User::getDepartment));

结果类似:

java
{ GIS=[Alice, Cindy], AI=[Bob], RemoteSensing=[Alice-New] }

5.2 按 id 分组:id -> List#

java
Map> groupById = userList.stream() .collect(Collectors.groupingBy(User::getId));

适用场景:

  • 同一个 key 对应多条记录。
  • 一个订单对应多个明细。
  • 一个分类对应多个内容。
  • 一个用户对应多条操作记录。

6. 一对多后,只保留某个字段#

6.1 department -> List#

java
Map> departmentNames = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.mapping(User::getName, Collectors.toList()) ));

6.2 department -> List#

java
Map> departmentIds = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.mapping(User::getId, Collectors.toList()) ));

7. 分组后做统计#

7.1 department -> 用户数量#

java
Map departmentCount = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.counting() ));

7.2 department -> 平均年龄#

java
Map departmentAvgAge = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.averagingInt(User::getAge) ));

8. 分组后取最大值 / 最小值#

8.1 每个部门年龄最大的用户#

java
Map> maxAgeUserByDepartment = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.maxBy(Comparator.comparing(User::getAge)) ));

返回 Optional 的原因是:下游收集器 maxBy 的结果本身就是 Optional。


8.2 如果确定每组一定有值#

java
Map 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#

java
ConcurrentMap concurrentMap = userList.parallelStream() .collect(Collectors.toConcurrentMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));

适用场景:

  • parallelStream()。
  • 并发收集。
  • 结果需要 ConcurrentMap。

9.2 使用 groupingByConcurrent#

java
ConcurrentMap> concurrentGroup = userList.parallelStream() .collect(Collectors.groupingByConcurrent(User::getDepartment));

10. null 处理注意事项#

10.1 key 可能为 null#

虽然部分 Map 实现允许 null key,但业务代码不建议依赖 null key。推荐先过滤:

java
Map 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。推荐提前处理:

java
Map 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>#

java
Map map = userList.stream() .collect(Collectors.toMap( User::getId, Function.identity(), (oldValue, newValue) -> newValue ));

11.2 List -> Map<id, field>#

java
Map map = userList.stream() .collect(Collectors.toMap( User::getId, User::getName, (oldValue, newValue) -> newValue ));

11.3 List -> Map<key, List>#

java
Map> map = userList.stream() .collect(Collectors.groupingBy(User::getDepartment));

11.4 List -> Map<key, List>#

java
Map> map = userList.stream() .collect(Collectors.groupingBy( User::getDepartment, Collectors.mapping(User::getName, Collectors.toList()) ));

11.5 List -> LinkedHashMap#

java
Map 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 重复导致异常#

错误写法:

java
Map map = userList.stream() .collect(Collectors.toMap(User::getId, Function.identity()));

当 id 重复时,会抛出异常。

正确写法:

java
Map 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 要点:

  1. toMap(keyMapper, valueMapper) 用于将元素收集为 Map。
  2. 如果 key 重复,二参数 toMap 会抛出异常。
  3. 如果 key 可能重复,应使用带 mergeFunction 的重载。
  4. 如果要指定 Map 类型,应使用带 mapFactory 的重载。
  5. groupingBy(classifier) 用于一对多分组。
  6. groupingByConcurrent 和 toConcurrentMap 用于并发 Map 场景。

16. 结论#

Java List 转 Map 最常见有三类:

  1. 一对一:Collectors.toMap()
  2. 一对多:Collectors.groupingBy()
  3. 分组后转换 / 统计:groupingBy + mapping/counting/maxBy

实际开发中最重要的是:

  • 明确 key 是否会重复。
  • 重复时必须定义 merge 策略。
  • 一对多不要强行用 toMap。
  • 有顺序要求时指定 LinkedHashMap。
  • 有并发要求时使用 concurrent collector。