1. 概述
在日常开发中,将 List
转换为 Map
是一个非常常见的需求,尤其当我们需要通过某个唯一标识快速查找对象时。本文将系统性地介绍几种主流的转换方式,涵盖从传统 Java 写法到现代函数式编程风格,并对比主流第三方库的实现。
我们假设 List
中的每个元素都有一个唯一标识字段(如 id
),该字段将作为 Map
的 key。
2. 示例数据结构
我们先定义一个简单的实体类 Animal
:
public class Animal {
private int id;
private String name;
public Animal(int id, String name) {
this.id = id;
this.name = name;
}
// getters and setters
public int getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Animal{id=" + id + ", name='" + name + "'}";
}
}
其中 id
字段作为唯一标识,适合作为 Map
的 key。
3. Java 8 之前的传统方式
在 Java 8 之前,我们只能通过遍历 List
手动 put 到 HashMap
中,方式简单粗暴但有效:
✅ 优点:逻辑清晰,兼容性好
❌ 缺点:代码冗长,不够函数式
public Map<Integer, Animal> convertListBeforeJava8(List<Animal> list) {
Map<Integer, Animal> map = new HashMap<>();
for (Animal animal : list) {
map.put(animal.getId(), animal);
}
return map;
}
测试用例验证转换结果:
@Test
public void givenAList_whenConvertBeforeJava8_thenReturnMapWithTheSameElements() {
Map<Integer, Animal> map = convertListService
.convertListBeforeJava8(list);
assertThat(map.values(), containsInAnyOrder(list.toArray()));
}
⚠️ 注意:put()
方法在 key 冲突时会覆盖旧值,后加入的元素生效。
4. 使用 Java 8 Stream 和 Collectors
Java 8 引入了 Stream
和 Collectors.toMap()
,让转换变得声明式且简洁:
✅ 优点:代码简洁,函数式风格,可读性强
✅ 推荐用于新项目
public Map<Integer, Animal> convertListAfterJava8(List<Animal> list) {
return list.stream()
.collect(Collectors.toMap(Animal::getId, Function.identity()));
}
Animal::getId
:提取 keyFunction.identity()
:value 就是元素本身
测试代码:
@Test
public void givenAList_whenConvertAfterJava8_thenReturnMapWithTheSameElements() {
Map<Integer, Animal> map = convertListService.convertListAfterJava8(list);
assertThat(map.values(), containsInAnyOrder(list.toArray()));
}
⚠️ 踩坑提醒:如果 List
中存在重复 key,Collectors.toMap()
会抛出 IllegalStateException
!
5. 使用 Guava 库
Google Guava 提供了更优雅的工具方法 Maps.uniqueIndex()
,专为这种场景设计。
5.1. Maven 依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</dependency>
最新版本可查看 Maven Central
5.2. 使用 Maps.uniqueIndex()
public Map<Integer, Animal> convertListWithGuava(List<Animal> list) {
return Maps.uniqueIndex(list, Animal::getId);
}
✅ 优点:语义清晰,代码极简
❌ 缺点:引入第三方依赖
测试用例:
@Test
public void givenAList_whenConvertWithGuava_thenReturnMapWithTheSameElements() {
Map<Integer, Animal> map = convertListService.convertListWithGuava(list);
assertThat(map.values(), containsInAnyOrder(list.toArray()));
}
⚠️ 注意:与 Collectors.toMap()
类似,uniqueIndex()
遇到重复 key 会抛出 IllegalArgumentException
。
6. 使用 Apache Commons Collections
Apache Commons Collections 也提供了 MapUtils.populateMap()
方法。
6.1. Maven 依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
最新版本见 Maven Central
6.2. 使用 MapUtils.populateMap()
public Map<Integer, Animal> convertListWithApacheCommons(List<Animal> list) {
Map<Integer, Animal> map = new HashMap<>();
MapUtils.populateMap(map, list, Animal::getId);
return map;
}
map
:目标 Maplist
:源 ListAnimal::getId
:key 生成器
测试代码:
@Test
public void givenAList_whenConvertWithApacheCommons_thenReturnMapWithTheSameElements() {
Map<Integer, Animal> map = convertListService.convertListWithApacheCommons(list);
assertThat(map.values(), containsInAnyOrder(list.toArray()));
}
⚠️ 行为差异:populateMap()
采用 put()
语义,后值覆盖前值,不会抛异常。
7. 处理 Key 冲突问题
当 List
中存在重复 key 时,不同方式的行为差异显著,这是实际开发中容易踩坑的地方。
7.1. 构造含重复 id 的 List
@Before
public void init() {
this.duplicatedIdList = new ArrayList<>();
duplicatedIdList.add(new Animal(1, "Cat"));
duplicatedIdList.add(new Animal(2, "Dog"));
duplicatedIdList.add(new Animal(3, "Pig"));
duplicatedIdList.add(new Animal(4, "Cow"));
duplicatedIdList.add(new Animal(4, "Goat")); // 与 Cow id 冲突
}
7.2. 不同方式的行为对比
方式 | 重复 key 行为 | 是否抛异常 |
---|---|---|
传统 for 循环 | 后值覆盖前值 | ❌ 不抛 |
Java 8 toMap | 抛 IllegalStateException |
✅ 抛 |
Guava uniqueIndex | 抛 IllegalArgumentException |
✅ 抛 |
Apache Commons populateMap | 后值覆盖前值 | ❌ 不抛 |
测试验证覆盖行为(传统方式):
@Test
public void givenADupIdList_whenConvertBeforeJava8_thenReturnMapWithRewrittenElement() {
Map<Integer, Animal> map = convertListService.convertListBeforeJava8(duplicatedIdList);
assertThat(map.values(), hasSize(4));
assertThat(map.values(), hasItem(duplicatedIdList.get(4))); // Goat 存在
}
测试验证异常行为(Java 8):
@Test(expected = IllegalStateException.class)
public void givenADupIdList_whenConvertAfterJava8_thenException() {
convertListService.convertListAfterJava8(duplicatedIdList);
}
Guava 同样抛异常:
@Test(expected = IllegalArgumentException.class)
public void givenADupIdList_whenConvertWithGuava_thenException() {
convertListService.convertListWithGuava(duplicatedIdList);
}
✅ 建议:
- 如果你期望严格唯一性,使用
Collectors.toMap()
或Guava.uniqueIndex()
- 如果你接受覆盖行为,使用传统方式或 Apache Commons
8. 总结
方式 | 代码简洁度 | 异常处理 | 是否推荐 |
---|---|---|---|
传统 for 循环 | ⭐⭐ | 覆盖 | ✅ 兼容老项目 |
Java 8 Stream | ⭐⭐⭐⭐⭐ | 抛异常 | ✅✅ 推荐新项目 |
Guava | ⭐⭐⭐⭐ | 抛异常 | ✅ 项目已引入可选 |
Apache Commons | ⭐⭐⭐ | 覆盖 | ⚠️ 可用但非主流 |
📌 最终建议:
- 新项目优先使用
Collectors.toMap()
,函数式风格清晰且能及时发现数据问题 - 若需自定义冲突处理(如保留旧值),可使用
toMap(keyMapper, valueMapper, mergeFunction)
- 第三方库选择取决于项目技术栈,避免为了一个小功能引入大依赖
完整示例代码已托管至 GitHub:https://github.com/baeldung/java-list-to-map-conversion