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() 基于 equals 和 hashCode,对基本类型和 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 更底层的归约操作,sum、count 等其实都是它的特例。
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 的 map、filter、peek 都是中间操作,只有遇到 collect、count、forEach 这类终止操作时才会真正执行:
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 日志这个特性很好用,但也容易误导人:不要把业务副作用塞进 peek 或 filter 里,更不要以为创建 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 万条时,并行通常没有优势。
- 有状态操作不安全:
forEach、peek等操作在并行流中顺序不确定,不能依赖执行顺序。 - 数据库连接等资源:并行流会同时发起多个连接,需要确保连接池足够。
// 自定义线程池使用 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 → Map | Collectors.toMap | 注意处理 key 冲突 |
| 按字段分组 | Collectors.groupingBy | 搭配 summingInt 做聚合 |
| 列表合并 | toMap 索引 + map 合并 | 先建索引再查找 |
| Key 规范化 | map + Collectors.toMap | 逐 entry 重建 Map |
| 去重 | distinct / toMap 按字段 | 按字段去重用 collectingAndThen |
| 扁平化 | flatMap | 嵌套 List 展平 |
| 聚合 | reduce | 求和、拼接、自定义归约 |
| 惰性求值 | 中间操作 + 终止操作 | 没有终止操作就不会执行 |
Stream API 的强大在于组合,每个操作都很简单,但串联起来可以处理复杂的数据转换。掌握这些技巧后,你会发现很多以前需要几十行代码的场景,现在只需要一个链式调用。
末尾一个建议:如果 Stream 链超过 5 个操作,或者逻辑已经开始让人困惑,别硬撑。拆成多步、给中间变量取个好名字,可读性永远比炫技重要。