Java Stream Collectors.toMap 详解:集合转 Map 用法
Java Stream API 中的 Collectors.toMap 方法用于将集合转换为 Map。支持三种重载形式处理键冲突和指定 Map 类型。常见场景包括 ID 到对象映射、属性提取及分组聚合。使用时需注意空值处理、键重复策略及并行流下的线程安全。性能与传统循环相当,代码更简洁。

Java Stream API 中的 Collectors.toMap 方法用于将集合转换为 Map。支持三种重载形式处理键冲突和指定 Map 类型。常见场景包括 ID 到对象映射、属性提取及分组聚合。使用时需注意空值处理、键重复策略及并行流下的线程安全。性能与传统循环相当,代码更简洁。

在日常开发中,我们经常遇到这样的场景:有一个对象列表,需要快速通过某个属性查找对象。比如通过城市 ID 查找城市信息,通过用户 ID 查找用户详情。
最直观的想法是:把 List 转换成 Map,用 ID 作 Key,对象作 Value。但是手动写循环又显得不够优雅。
Java Stream 的 Collectors.toMap() 就是为了解决这个问题而生!
// ❌ 传统方式:写循环,代码臃肿
Map<Integer, City> cityMap = new HashMap<>();
for (City city : cityList) {
cityMap.put(city.getCityId(), city);
}
// ✅ Stream 方式:一行代码搞定
Map<Integer, City> cityMap = cityList.stream().collect(Collectors.toMap(City::getCityId, Function.identity()));
Collectors.toMap 是 Java Stream API 提供的收集器,用于将流中的元素转换为键值对,并收集到一个 Map 中。
| 场景 | 说明 | 示例 |
|---|---|---|
| ID-对象映射 | 通过 ID 快速查找对象 | 用户 ID -> 用户对象 |
| ID-属性映射 | 提取对象的某个属性 | 城市 ID -> 城市名称 |
| 分组聚合 | 按某个维度统计 | 销售员 -> 总销售额 |
| 去重转换 | 处理键冲突情况 | 自定义冲突策略 |
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
Function<? super T, ? extends K> keyMapper, // 键提取函数
Function<? super T, ? extends U> valueMapper // 值提取函数
)
特点:
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
Function<? super T, ? extends K> keyMapper, // 键提取函数
Function<? super T, ? extends U> valueMapper, // 值提取函数
BinaryOperator<U> mergeFunction // 冲突合并函数
)
特点:
public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(
Function<? super T, ? extends K> keyMapper, // 键提取函数
Function<? super T, ? extends U> valueMapper, // 值提取函数
BinaryOperator<U> mergeFunction, // 冲突合并函数
Supplier<M> mapSupplier // Map 工厂
)
特点:
| 无重复键 | 可能有重复键 | 需要指定 Map 类型 |
|---|---|---|
| Stream 流 | 使用哪个 toMap? | 双参数版本 |
| HashMap | 三参数版本 | 四参数版本 |
| 自定义合并策略 | TreeMap/LinkedHashMap 等 |
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class City {
private Integer cityId;
private String cityName;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SalesRecord {
private String salesPerson;
private BigDecimal amount;
}
import org.junit.jupiter.api.Test;
import java.util.*;
import java.util.stream.Collectors;
public class ToMapExampleTest {
@Test
public void testIdToNameMap() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳")
);
// 转换为 Map<ID, 城市名称>
Map<Integer, String> cityNameMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId, // key: 城市 ID
City::getCityName // value: 城市名称
)
);
System.out.println("城市名称映射:" + cityNameMap);
// 通过 ID 快速获取城市名称
System.out.println("城市 ID 3 的名称:" + cityNameMap.get(3));
}
}
输出:
城市名称映射:{1=北京,2=上海,3=广州,4=深圳}
城市 ID 3 的名称:广州
@Test
public void testIdToObjectMap() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳")
);
// 方式 1:使用 lambda
Map<Integer, City> cityMap1 = cityList.stream().collect(
Collectors.toMap(
city -> city.getCityId(),
city -> city
)
);
// 方式 2:使用方法引用(推荐)
Map<Integer, City> cityMap2 = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
Function.identity() // 等同于 city -> city
)
);
City city = cityMap2.get(1);
System.out.println("城市 ID: " + city.getCityId());
System.out.println("城市名称:" + city.getCityName());
}
输出:
城市 ID: 1
城市名称:北京
@Test
public void testDuplicateKeyHandling() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳"),
new City(4, "天津") // 重复 ID
);
// 抛出异常版本(默认)
try {
Map<Integer, String> map = cityList.stream().collect(
Collectors.toMap(City::getCityId, City::getCityName)
);
} catch (IllegalStateException e) {
System.out.println("默认行为:键重复抛出异常 - " + e.getMessage());
}
// 保留旧值
Map<Integer, String> keepOldMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(oldValue, newValue) -> oldValue // 保留旧值
)
);
System.out.println("保留旧值:" + keepOldMap.get(4));
// 使用新值
Map<Integer, String> useNewMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(oldValue, newValue) -> newValue // 使用新值
)
);
System.out.println("使用新值:" + useNewMap.get(4));
// 合并值
Map<Integer, String> mergeMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(oldValue, newValue) -> oldValue + ", " + newValue // 合并
)
);
System.out.println("合并值:" + mergeMap.get(4));
}
输出:
默认行为:键重复抛出异常 - Duplicate key 4 (attempted merging values 深圳 and 天津)
保留旧值:深圳
使用新值:天津
合并值:深圳,天津
@Test
public void testAggregation() {
List<SalesRecord> records = Arrays.asList(
new SalesRecord("张三", new BigDecimal("1000")),
new SalesRecord("李四", new BigDecimal("2000")),
new SalesRecord("张三", new BigDecimal("980")),
new SalesRecord("王五", new BigDecimal("1500")),
new SalesRecord("李四", new BigDecimal("1200"))
);
// 计算每个人的总销售额
Map<String, BigDecimal> totalMap = records.stream().collect(
Collectors.toMap(
SalesRecord::getSalesPerson,
SalesRecord::getAmount,
BigDecimal::add // 合并函数:相加
)
);
System.out.println("总销售额:" + totalMap);
// 计算每个人的最大销售额
Map<String, BigDecimal> maxMap = records.stream().collect(
Collectors.toMap(
SalesRecord::getSalesPerson,
SalesRecord::getAmount,
BigDecimal::max // 取最大值
)
);
System.out.println("最大销售额:" + maxMap);
// 计算每个人的最小销售额
Map<String, BigDecimal> minMap = records.stream().collect(
Collectors.toMap(
SalesRecord::getSalesPerson,
SalesRecord::getAmount,
BigDecimal::min // 取最小值
)
);
System.out.println("最小销售额:" + minMap);
}
输出:
总销售额:{李四=3200, 张三=1980, 王五=1500}
最大销售额:{李四=2000, 张三=1000, 王五=1500}
最小销售额:{李四=1200, 张三=980, 王五=1500}
@Test
public void testSpecifyMapType() {
List<City> cityList = Arrays.asList(
new City(2, "上海"),
new City(1, "北京"),
new City(4, "深圳"),
new City(3, "广州")
);
// 默认 HashMap(无序)
Map<Integer, String> hashMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old
)
);
System.out.println("HashMap (无序): " + hashMap);
// 使用 LinkedHashMap(保持插入顺序)
Map<Integer, String> linkedHashMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old,
LinkedHashMap::new // 指定 LinkedHashMap
)
);
System.out.println("LinkedHashMap (插入顺序): " + linkedHashMap);
// 使用 TreeMap(按键排序)
Map<Integer, String> treeMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old,
TreeMap::new // 指定 TreeMap
)
);
System.out.println("TreeMap (键排序): " + treeMap);
// 使用 ConcurrentHashMap(线程安全)
Map<Integer, String> concurrentMap = cityList.parallelStream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old,
ConcurrentHashMap::new // 线程安全的 Map
)
);
System.out.println("ConcurrentHashMap: " + concurrentMap);
}
输出:
HashMap (无序): {1=北京,2=上海,3=广州,4=深圳}
LinkedHashMap (插入顺序): {2=上海,1=北京,4=深圳,3=广州}
TreeMap (键排序): {1=北京,2=上海,3=广州,4=深圳}
ConcurrentHashMap: {1=北京,2=上海,3=广州,4=深圳}
@Test
public void testComplexTransformation() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海")
);
// 转换为 DTO 对象
Map<Integer, CityDTO> dtoMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
city -> new CityDTO(
city.getCityId(),
city.getCityName(),
"中国",
getCityCode(city.getCityName())
)
)
);
System.out.println("DTO 映射:" + dtoMap);
}
// 简化的 DTO 类
@Data
@AllArgsConstructor
class CityDTO {
private Integer id;
private String name;
private String country;
private String code;
}
private String getCityCode(String cityName) {
switch (cityName) {
case "北京": return "BJ";
case "上海": return "SH";
default: return "UNKNOWN";
}
}
@Test
public void testConditionalMapping() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳"),
new City(5, null) // 可能为 null
);
// 过滤掉 null 值
Map<Integer, String> filteredMap = cityList.stream()
.filter(city -> city.getCityName() != null)
.collect(Collectors.toMap(City::getCityId, City::getCityName));
System.out.println("过滤 null: " + filteredMap);
// 提供默认值
Map<Integer, String> defaultMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
city -> Optional.ofNullable(city.getCityName()).orElse("未知")
)
);
System.out.println("默认值:" + defaultMap);
}
@Test
public void testCompositeKey() {
List<Order> orders = Arrays.asList(
new Order("202501", "A001", new BigDecimal("100")),
new Order("202501", "A002", new BigDecimal("200")),
new Order("202502", "A001", new BigDecimal("150"))
);
// 使用月份 + 客户 ID 作为组合键
Map<String, BigDecimal> monthlyCustomerTotal = orders.stream().collect(
Collectors.toMap(
order -> order.getMonth() + "_" + order.getCustomerId(),
Order::getAmount,
BigDecimal::add
)
);
System.out.println("月度客户汇总:" + monthlyCustomerTotal);
}
@Data
@AllArgsConstructor
class Order {
private String month;
private String customerId;
private BigDecimal amount;
}
@Test
public void testNullValueProblem() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳"),
new City(5, null) // 值为 null
);
// ❌ 会抛出 NullPointerException
try {
Map<Integer, String> map = cityList.stream().collect(
Collectors.toMap(City::getCityId, City::getCityName)
);
} catch (NullPointerException e) {
System.out.println("NullPointerException: " + e.getMessage());
}
// ✅ 解决方案 1:过滤 null 值
Map<Integer, String> solution1 = cityList.stream()
.filter(city -> city.getCityName() != null)
.collect(Collectors.toMap(City::getCityId, City::getCityName));
// ✅ 解决方案 2:提供默认值
Map<Integer, String> solution2 = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
city -> Optional.ofNullable(city.getCityName()).orElse("未知")
)
);
}
@Test
public void testDuplicateKeyProblem() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(1, "天津") // 重复键
);
// ❌ 抛出 IllegalStateException
try {
Map<Integer, String> map = cityList.stream().collect(
Collectors.toMap(City::getCityId, City::getCityName)
);
} catch (IllegalStateException e) {
System.out.println("IllegalStateException: " + e.getMessage());
}
// ✅ 必须提供合并函数
Map<Integer, String> map = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(oldValue, newValue) -> oldValue // 合并策略
)
);
}
@Test
public void testParallelStream() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳")
);
// 并行流中使用 toMap
Map<Integer, String> map = cityList.parallelStream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(v1, v2) -> v1, // 合并函数在并行流中很重要!
ConcurrentHashMap::new // 推荐使用 ConcurrentHashMap
)
);
System.out.println("并行流结果:" + map);
}
@Test
public void testPerformance() {
List<City> cityList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
cityList.add(new City(i, "City" + i));
}
// 传统循环
long start = System.currentTimeMillis();
Map<Integer, City> loopMap = new HashMap<>();
for (City city : cityList) {
loopMap.put(city.getCityId(), city);
}
long loopTime = System.currentTimeMillis() - start;
// Stream
start = System.currentTimeMillis();
Map<Integer, City> streamMap = cityList.stream().collect(
Collectors.toMap(City::getCityId, Function.identity())
);
long streamTime = System.currentTimeMillis() - start;
System.out.println("传统循环耗时:" + loopTime + "ms");
System.out.println("Stream 耗时:" + streamTime + "ms");
System.out.println("性能差异:" + (loopTime - streamTime) + "ms");
}
结果(10 万条数据):
传统循环耗时:35ms
Stream 耗时:28ms
性能差异:7ms
| Map 类型 | 插入 10 万条耗时 | 特点 |
|---|---|---|
| HashMap | 28ms | 最快,无序 |
| LinkedHashMap | 32ms | 稍慢,保持顺序 |
| TreeMap | 85ms | 最慢,自动排序 |
| ConcurrentHashMap | 35ms | 线程安全 |
| 场景 | 推荐写法 | 说明 |
|---|---|---|
| 简单 ID-对象映射 | toMap(keyMapper, Function.identity()) | 最常用 |
| ID-属性映射 | toMap(keyMapper, valueMapper) | 提取特定属性 |
| 可能有重复键 | 提供合并函数 | 避免异常 |
| 需要有序 Map | 指定 LinkedHashMap | 保持插入顺序 |
| 需要排序 Map | 指定 TreeMap | 按键排序 |
| 并行流处理 | 指定 ConcurrentHashMap | 线程安全 |
// 最常用模板:ID 到对象映射
Map<KeyType, Entity> map = list.stream().collect(
Collectors.toMap(
Entity::getKeyMethod,
Function.identity(),
(existing, replacement) -> existing // 保留旧值
)
);
// 分组聚合模板
Map<KeyType, ValueType> aggMap = list.stream().collect(
Collectors.toMap(
Item::getKey,
Item::getValue,
(v1, v2) -> v1 + v2 // 自定义合并逻辑
)
);
// 有序 Map 模板
Map<KeyType, ValueType> orderedMap = list.stream().collect(
Collectors.toMap(
Item::getKey,
Item::getValue,
(v1, v2) -> v1,
LinkedHashMap::new // 或 TreeMap::new
)
);
| 特性 | 说明 |
|---|---|
| 核心功能 | 将 List 转换为 Map,实现快速查找 |
| 三个版本 | 2 参数(基础)、3 参数(冲突处理)、4 参数(指定 Map) |
| 常见用途 | ID 映射、属性提取、分组聚合 |
| 注意事项 | null 值处理、键冲突、并发安全 |
| 性能表现 | 与传统循环相当,代码更优雅 |
一句话总结:
Collectors.toMap是 Java Stream 中最实用的收集器之一,掌握它能让你用一行代码搞定过去需要写循环的复杂转换!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online