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 引入了 StreamCollectors.toMap(),让转换变得声明式且简洁:

优点:代码简洁,函数式风格,可读性强
推荐用于新项目

public Map<Integer, Animal> convertListAfterJava8(List<Animal> list) {
    return list.stream()
        .collect(Collectors.toMap(Animal::getId, Function.identity()));
}
  • Animal::getId:提取 key
  • Function.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:目标 Map
  • list:源 List
  • Animal::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


原始标题:How to Convert List to Map in Java | Baeldung