ZetCode

在 Java 中将 List 转换为 HashMap

最后修改日期:2025年6月1日

本教程探讨了在 Java 中将 List 转换为 HashMap 的多种技术,涵盖了传统的 for 循环、现代 Stream API 和高级场景,例如处理自定义对象、重复键和分组元素。 这些方法对于在 Java 应用程序中转换数据结构至关重要,例如将索引映射到元素或按属性对对象进行分组。

使用 For 循环进行基本转换

List 转换为 HashMap 的一种直接方法是使用 for 循环,将列表索引映射到元素。 此方法简单且适用于小型列表,或者在需要显式控制时使用。

Main.java
void main() {

    List<String> fruits = List.of("Apple", "Banana", "Cherry", "Date");
    Map<Integer, String> map = new HashMap<>();
    
    for (int i = 0; i < fruits.size(); i++) {
        map.put(i + 1, fruits.get(i)); // Start indexing from 1
    }
    
    System.out.println(map);
}

此示例使用 List.of 创建一个不可变的 fruits 列表,然后初始化一个 HashMap 来存储索引-元素对。 for 循环迭代列表,将从 1 开始的索引映射到每个 fruit,生成一个 map,其中每个 fruit 都与一个唯一的索引相关联。 这种方法是明确且易于理解的,使其成为简单转换的理想选择,但对于复杂的转换来说,它可能会变得冗长。

使用 Stream API

Stream API 提供了一种函数式且简洁的方式来将 List 转换为 HashMap。 使用 IntStreamCollectors.toMap,您可以实现与 for 循环相同的结果,但代码更少。

Main.java
void main() {

    List<String> fruits = List.of("Apple", "Banana", "Cherry", "Date");
    
    Map<Integer, String> map = IntStream.rangeClosed(1, fruits.size())
        .boxed()
        .collect(Collectors.toMap(
            i -> i,
            i -> fruits.get(i - 1)
        ));
    
    System.out.println(map);
}

在此示例中,IntStream.rangeClosed(1, fruits.size()) 生成一个从 1 到列表大小的索引序列,然后使用 boxed() 将其转换为 Stream<Integer>,以便与 Collectors.toMap 一起使用。

toMap 收集器将每个索引映射到通过 fruits.get(i - 1) 访问的相应列表元素,从而产生与 for 循环示例相同的结果。 这种方法更简洁,并符合函数式编程原则,为复杂的映射提供了灵活性,但由于流设置,对于非常小的列表,它可能会引入轻微的开销。

转换自定义对象的列表

当使用自定义对象的 List 时,您可以使用 Stream API 基于特定字段将对象映射到 HashMap 作为键。

Main.java
record Product(int id, String name, double price) {}

void main() {

    List<Product> products = List.of(
        new Product(101, "Laptop", 999.99),
        new Product(102, "Phone", 699.99),
        new Product(103, "Tablet", 499.99)
    );
    
    Map<Integer, Product> map = products.stream()
        .collect(Collectors.toMap(
            Product::id,
            p -> p,
            (existing, replacement) -> existing // Keep existing entry on duplicate keys
        ));
    
    map.forEach((k, v) -> System.out.printf("ID: %d, Product: %s%n", k, v));
}

此示例定义了一个具有 idnameprice 字段的 Product 记录,并创建了一个 product 实例列表。 stream 操作使用 Collectors.toMap 将每个 product 的 id 映射到 product 对象本身,并使用合并函数 (existing, replacement) -> existing 来处理潜在的重复键,方法是保留第一次出现的键。

生成的 map 将每个 product 的 ID 与相应的 product 相关联,这对于将数据库记录映射到其主键等场景非常有用。 输出的格式清晰地显示了每个键值对。

处理重复键

在将 List 转换为 HashMap 时,可能会出现重复的键。 Stream API 的 toMap 收集器允许使用合并函数来解决冲突,例如计算出现次数。

Main.java
void main() {

    List<String> fruits = List.of("Apple", "Banana", "Apple", "Cherry", "Apple");
    
    Map<String, Integer> map = fruits.stream()
        .collect(Collectors.toMap(
            f -> f,
            f -> 1,
            Integer::sum
        ));
    
    System.out.println(map);
}

此示例处理一个包含重复项的 fruits 列表,使用 Collectors.toMap 创建一个 map,其中每个 fruit 都是一个键,其值表示出现次数。 每个 fruit 最初都映射到计数 1,合并函数 Integer::sum 将重复键的计数相加,从而生成一个频率 map(例如,“Apple”出现三次)。

这种方法非常适合计算出现次数或聚合数据。 如果没有合并函数,尝试映射重复键将抛出 IllegalStateException,因此合并函数对于优雅地处理重复项至关重要。

按属性对列表元素进行分组

Collectors.groupingBy 方法将列表元素分组到 Map 中,其中键是一个属性,值是匹配对象的 List

Main.java
record Person(String name, int age, String department) {}

void main() {

    List<Person> people = List.of(
        new Person("John", 25, "IT"),
        new Person("Jane", 30, "HR"),
        new Person("Bob", 25, "IT"),
        new Person("Alice", 28, "HR")
    );
    
    Map<String, List<Person>> byDept = people.stream()
        .collect(Collectors.groupingBy(Person::department));
    
    byDept.forEach((dept, list) -> 
        System.out.printf("%s: %s%n", dept, list.stream()
            .map(p -> p.name())
            .collect(Collectors.joining(", ")))
    );
}

此示例使用 Collectors.groupingBydepartment 字段对 Person 对象列表进行分组,从而生成一个 Map,其中每个键是一个部门,值是该部门中人员的列表。

输出的格式设置为仅显示每个部门中人员的姓名,并用逗号连接以提高可读性。 这种方法对于对数据进行分类特别有用,例如按部门对员工进行分组或按类别对产品进行分组,从而更容易分析或显示分组信息。

性能注意事项

方法 用例 性能 优点 缺点
For 循环 简单的基于索引的映射 O(n),开销最小 对于小型列表速度快,显式控制 冗长,对于复杂的逻辑容易出错
带有 toMap 的 Stream API 函数式转换 O(n),轻微的流开销 简洁,对于复杂的映射灵活 由于流设置,对于小型列表速度较慢
Collectors.toMap 键值映射 O(n),对于直接映射效率高 使用合并函数处理重复项 需要仔细处理键的唯一性
Collectors.groupingBy 按属性分组 O(n),取决于分组的复杂性 非常适合分类任务 存储分组列表需要更多内存

对于小型列表(<100 个元素),由于开销较低,for 循环通常更快。Streams 在复杂转换或并行处理方面更具可读性和可维护性。

如果需要保留插入顺序,请在 toMap 中使用 LinkedHashMap 而不是 HashMap。 避免在 toMap 中使用 null 键/值,以防止 NullPointerException

来源

Java Collectors 文档

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直撰写编程文章。 迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程