← 返回

Java Stream 实用技巧:List 与 Map 的花式转换

前言

Java 8 引入的 Stream API 彻底改变了我们处理集合数据的方式。相比传统的 for 循环,Stream 提供了声明式、链式、可组合的数据处理风格,代码更简洁,意图更清晰。

在日常开发中,我们经常面对这样的场景:从数据库查出一个 List<Map>,需要转成按某个 key 分组的 Map;两个接口返回的列表需要按 id 合并;或者需要对字段做批量转换。这些需求用 Stream 几行代码就能搞定,而传统写法往往要写一堆临时变量和嵌套循环。

本文整理了 7 个实用的 Stream 技巧,每个都附带完整可运行的示例代码,希望能帮助你在实际项目中写出更优雅的集合操作。


一、List 转 Map(key 冲突处理)

基础也常见的操作。假设有一个用户列表,需要按 id 建索引:

import java.util.*;
import java.util.stream.*;

List<Map<String, Object>> users = new ArrayList<>();
users.add(new HashMap<String, Object>() {{ put("id", 1L); put("name", "Alice"); }});
users.add(new HashMap<String, Object>() {{ put("id", 2L); put("name", "Bob"); }});
users.add(new HashMap<String, Object>() {{ put("id", 3L); put("name", "Charlie"); }});

// 基本转换
Map<Long, Map<String, Object>> result = users.stream()
    .collect(Collectors.toMap(
        m -> (Long) m.get("id"),   // key mapper
        m -> m                      // value mapper
    ));

System.out.println(result);
// {1={name=Alice, id=1}, 2={name=Bob, id=2}, 3={name=Charlie, id=3}}

踩坑提示:如果 key 有重复,toMap 会抛 IllegalStateException。解决方案是提供第三个参数,合并函数:

// key 冲突时保留后者
Map<Long, Map<String, Object>> result = users.stream()
    .collect(Collectors.toMap(
        m -> (Long) m.get("id"),
        m -> m,
        (oldVal, newVal) -> newVal  // 合并策略:保留新值
    ));

如果只需要某个字段,直接映射 value 即可:

// 只取 name 字段
Map<Long, String> nameMap = users.stream()
    .collect(Collectors.toMap(
        m -> (Long) m.get("id"),
        m -> (String) m.get("name"),
        (oldVal, newVal) -> newVal
    ));

System.out.println(nameMap);
// {1=Alice, 2=Bob, 3=Charlie}

二、List 转 Map 并分组(groupingBy)

当一个 key 对应多个 value 时,我们需要的并非 toMap,重点是 groupingBy

场景:有一组订单记录,需要按用户 id 分组,得到每个用户的所有订单。

import java.util.*;
import java.util.stream.*;

List<Map<String, Object>> orders = new ArrayList<>();
orders.add(new HashMap<String, Object>() {{ put("userId", 1L); put("amount", 100); }});
orders.add(new HashMap<String, Object>() {{ put("userId", 2L); put("amount", 200); }});
orders.add(new HashMap<String, Object>() {{ put("userId", 1L); put("amount", 150); }});
orders.add(new HashMap<String, Object>() {{ put("userId", 3L); put("amount", 300); }});
orders.add(new HashMap<String, Object>() {{ put("userId", 2L); put("amount", 50); }});

// 按 userId 分组
Map<Long, List<Map<String, Object>>> grouped = orders.stream()
    .collect(Collectors.groupingBy(m -> (Long) m.get("userId")));

System.out.println("用户 1 的订单: " + grouped.get(1L));
// 用户 1 的订单: [{amount=100, userId=1}, {amount=150, userId=1}]

进阶:分组后做聚合统计:

// 按用户统计订单总金额
Map<Long, Integer> totalByUser = orders.stream()
    .collect(Collectors.groupingBy(
        m -> (Long) m.get("userId"),
        Collectors.summingInt(m -> (Integer) m.get("amount"))
    ));

System.out.println(totalByUser);
// {1=250, 2=250, 3=300}

如果只需要分组后的第一个元素,可以用 toMap 配合合并函数,或者:

Map<Long, Optional<Map<String, Object>>> firstByUser = orders.stream()
    .collect(Collectors.groupingBy(
        m -> (Long) m.get("userId"),
        Collectors.collectingAndThen(Collectors.toList(), list -> Optional.of(list.get(0)))
    ));

三、两个 List 按相同 key 合并并排序

场景:两个接口分别返回用户基本信息和用户积分,需要按 userId 合并,然后按积分排序。

List<Map<String, Object>> userInfo = new ArrayList<>();
userInfo.add(new HashMap<String, Object>() {{ put("userId", 1L); put("name", "Alice"); }});
userInfo.add(new HashMap<String, Object>() {{ put("userId", 2L); put("name", "Bob"); }});
userInfo.add(new HashMap<String, Object>() {{ put("userId", 3L); put("name", "Charlie"); }});

List<Map<String, Object>> userScore = new ArrayList<>();
userScore.add(new HashMap<String, Object>() {{ put("userId", 1L); put("score", 88); }});
userScore.add(new HashMap<String, Object>() {{ put("userId", 2L); put("score", 95); }});
userScore.add(new HashMap<String, Object>() {{ put("userId", 3L); put("score", 72); }});

// 先将 score 列表转为 Map 索引
Map<Long, Map<String, Object>> scoreIndex = userScore.stream()
    .collect(Collectors.toMap(
        m -> (Long) m.get("userId"),
        m -> m,
        (a, b) -> b
    ));

// 合并并按 score 降序排序
List<Map<String, Object>> merged = userInfo.stream()
    .map(info -> {
        Long userId = (Long) info.get("userId");
        Map<String, Object> combined = new HashMap<>(info);
        Map<String, Object> score = scoreIndex.get(userId);
        if (score != null) {
            combined.putAll(score);
        }
        return combined;
    })
    .sorted(Comparator.comparingInt(
        (Map<String, Object> m) -> (Integer) m.getOrDefault("score", 0)
    ).reversed())
    .collect(Collectors.toList());

merged.forEach(System.out::println);
// {name=Bob, score=95, userId=2}
// {name=Alice, score=88, userId=1}
// {name=Charlie, score=72, userId=3}

要点:合并的核心思路是先把一方转成 Map<key, item> 作为索引,然后 map 另一方时查找合并。排序用 Comparator.comparingInt(...).reversed() 实现降序,比直接取负更稳(避免极端值溢出)。


四、List 的 key 全部转大写 / 小写

有时候从外部系统拿到的数据,key 大小写不统一,需要统一规范化:

List<Map<String, Object>> raw = new ArrayList<>();
raw.add(new HashMap<String, Object>() {{ put("Name", "Alice"); put("AGE", 30); }});
raw.add(new HashMap<String, Object>() {{ put("Name", "Bob"); put("AGE", 25); }});

// key 全部转小写
List<Map<String, Object>> normalized = raw.stream()
    .map(m -> m.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey().toLowerCase(Locale.ROOT),
            Map.Entry::getValue,
            (oldVal, newVal) -> newVal
        ))
    )
    .collect(Collectors.toList());

System.out.println(normalized);
// [{name=Alice, age=30}, {name=Bob, age=25}]

转大写只需把 toLowerCase(Locale.ROOT) 换成 toUpperCase(Locale.ROOT)。这里显式使用 Locale.ROOT,避免土耳其语等区域设置导致 I/i 转换异常;合并函数用于处理大小写归一后 key 冲突的情况。


五、去重(distinct)

Stream 的 distinct() 基于 equalshashCode,对基本类型和 String 直接可用:

List<String> names = Arrays.asList("Alice", "Bob", "Alice", "Charlie", "Bob");

List<String> unique = names.stream()
    .distinct()
    .collect(Collectors.toList());

System.out.println(unique);
// [Alice, Bob, Charlie]

按某个字段去重,这是实际项目中更常见的需求。比如按 id 去重,保留第一条:

List<Map<String, Object>> list = new ArrayList<>();
list.add(new HashMap<String, Object>() {{ put("id", 1); put("name", "Alice"); }});
list.add(new HashMap<String, Object>() {{ put("id", 1); put("name", "Alice v2"); }});
list.add(new HashMap<String, Object>() {{ put("id", 2); put("name", "Bob"); }});

List<Map<String, Object>> deduped = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(
            m -> m.get("id"),
            m -> m,
            (a, b) -> a,  // 保留第一个
            LinkedHashMap::new
        ),
        map -> new ArrayList<>(map.values())
    ));

System.out.println(deduped);
// [{name=Alice, id=1}, {name=Bob, id=2}]

也可以借助 TreeSet 做自定义去重:

List<Map<String, Object>> deduped = list.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(
            Comparator.comparingInt(m -> (Integer) m.get("id"))
        )),
        ArrayList::new
    ));

六、filter 过滤与 flatMap 扁平化

filter:按条件筛选

List<Map<String, Object>> list = new ArrayList<>();
list.add(new HashMap<String, Object>() {{ put("name", "Alice"); put("age", 30); }});
list.add(new HashMap<String, Object>() {{ put("name", "Bob"); put("age", 17); }});
list.add(new HashMap<String, Object>() {{ put("name", "Charlie"); put("age", 22); }});

// 筛选成年人
List<Map<String, Object>> adults = list.stream()
    .filter(m -> (Integer) m.get("age") >= 18)
    .collect(Collectors.toList());

System.out.println(adults);
// [{name=Alice, age=30}, {name=Charlie, age=22}]

flatMap:将嵌套结构展平

场景:每个用户有多组地址,需要展平为一个地址列表。

List<Map<String, Object>> users = new ArrayList<>();
users.add(new HashMap<String, Object>() {{
    put("name", "Alice");
    put("addresses", Arrays.asList("北京", "上海"));
}});
users.add(new HashMap<String, Object>() {{
    put("name", "Bob");
    put("addresses", Arrays.asList("广州");
}});

List<String> allAddresses = users.stream()
    .flatMap(m -> ((List<String>) m.get("addresses")).stream())
    .collect(Collectors.toList());

System.out.println(allAddresses);
// [北京, 上海, 广州]

filter + flatMap 组合 是处理多层嵌套数据的利器,能将复杂的数据结构简化为单层流。


七、reduce 聚合

reduce 是 Stream 更底层的归约操作,sumcount 等其实都是它的特例。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 求和
int sum = numbers.stream()
    .reduce(0, Integer::sum);
System.out.println("求和: " + sum);
// 求和: 15

// 求最大值
int max = numbers.stream()
    .reduce(Integer::max)
    .orElse(0);
System.out.println("最大值: " + max);
// 最大值: 5

// 字符串拼接
String joined = Stream.of("Java", "Stream", "API")
    .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
System.out.println("拼接: " + joined);
// 拼接: Java Stream API

在实际项目中,reduce 常用于将一组数据合并为一个汇总对象,比如将多个订单行项合并为订单总览。


八、惰性求值:中间操作不会立刻执行

Stream 的 mapfilterpeek 都是中间操作,只有遇到 collectcountforEach 这类终止操作时才会真正执行:

Stream<String> stream = names.stream()
    .filter(name -> {
        System.out.println("filter: " + name);
        return name.startsWith("A");
    });

// 这里只是声明流水线,上面的 filter 还没有运行

List<String> result = stream.collect(Collectors.toList());
// collect 触发执行才会打印 filter 日志

这个特性很好用,但也容易误导人:不要把业务副作用塞进 peekfilter 里,更不要以为创建 Stream 后逻辑已经执行了。


九、性能注意事项

parallelStream 的坑

parallelStream 看起来很诱人,一行代码就能并行处理。但实际使用中要注意:

// ❌ 危险:在 parallelStream 中操作共享可变状态
List<String> result = Collections.synchronizedList(new ArrayList<>());
list.parallelStream().forEach(item -> result.add(process(item)));

// ✅ 安全:使用 collect 而不是 forEach + add
List<String> safeResult = list.parallelStream()
    .map(this::process)
    .collect(Collectors.toList());

关键要点

  • ForkJoinPool 共享线程池:默认使用公共池,其他并行任务或 CompletableFuture 也会竞争同一个池,容易导致互相阻塞。生产环境建议自定义线程池。
  • 数据量小时反而更慢:线程调度和合并的开销可能超过并行收益。经验上,数据量少于 1 万条时,并行通常没有优势。
  • 有状态操作不安全forEachpeek 等操作在并行流中顺序不确定,不能依赖执行顺序。
  • 数据库连接等资源:并行流会同时发起多个连接,需要确保连接池足够。
// 自定义线程池使用 parallelStream
ForkJoinPool customPool = new ForkJoinPool(4);
customPool.submit(() ->
    list.parallelStream().map(this::heavyProcess).collect(Collectors.toList())
).get();

完整可运行示例

将本文所有技巧整合为一个可直接运行的 Main 类:

import java.util.*;
import java.util.stream.*;

public class StreamDemo {
    public static void main(String[] args) {
        // 1. List 转 Map
        List<Map<String, Object>> users = Arrays.asList(
            mapOf("id", 1L, "name", "Alice"),
            mapOf("id", 2L, "name", "Bob"),
            mapOf("id", 3L, "name", "Charlie")
        );

        Map<Long, String> nameMap = users.stream()
            .collect(Collectors.toMap(
                m -> (Long) m.get("id"),
                m -> (String) m.get("name"),
                (a, b) -> b
            ));
        System.out.println("=== List 转 Map ===");
        System.out.println(nameMap);

        // 2. groupingBy 分组
        List<Map<String, Object>> orders = Arrays.asList(
            mapOf("userId", 1L, "amount", 100),
            mapOf("userId", 2L, "amount", 200),
            mapOf("userId", 1L, "amount", 150)
        );

        Map<Long, List<Map<String, Object>>> grouped = orders.stream()
            .collect(Collectors.groupingBy(m -> (Long) m.get("userId")));
        System.out.println("\n=== 分组 ===");
        System.out.println(grouped);

        // 3. distinct 去重
        List<String> names = Arrays.asList("Alice", "Bob", "Alice", "Charlie");
        System.out.println("\n=== 去重 ===");
        System.out.println(names.stream().distinct().collect(Collectors.toList()));

        // 4. filter 过滤
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> evens = numbers.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println("\n=== 过滤偶数 ===");
        System.out.println(evens);

        // 5. reduce 聚合
        int sum = numbers.stream().reduce(0, Integer::sum);
        System.out.println("\n=== reduce 求和 ===");
        System.out.println(sum);
    }

    private static Map<String, Object> mapOf(Object... kvs) {
        Map<String, Object> map = new LinkedHashMap<>();
        for (int i = 0; i < kvs.length; i += 2) {
            map.put((String) kvs[i], kvs[i + 1]);
        }
        return map;
    }
}

总结

场景核心 API一句话
List → MapCollectors.toMap注意处理 key 冲突
按字段分组Collectors.groupingBy搭配 summingInt 做聚合
列表合并toMap 索引 + map 合并先建索引再查找
Key 规范化map + Collectors.toMap逐 entry 重建 Map
去重distinct / toMap 按字段按字段去重用 collectingAndThen
扁平化flatMap嵌套 List 展平
聚合reduce求和、拼接、自定义归约
惰性求值中间操作 + 终止操作没有终止操作就不会执行

Stream API 的强大在于组合,每个操作都很简单,但串联起来可以处理复杂的数据转换。掌握这些技巧后,你会发现很多以前需要几十行代码的场景,现在只需要一个链式调用。

末尾一个建议:如果 Stream 链超过 5 个操作,或者逻辑已经开始让人困惑,别硬撑。拆成多步、给中间变量取个好名字,可读性永远比炫技重要。