跳到主要内容Java Stream Collectors.toMap 详解:集合转 Map 用法 | 极客日志Javajava
Java Stream Collectors.toMap 详解:集合转 Map 用法
Java Stream API 中的 Collectors.toMap 方法用于将集合转换为 Map。支持三种重载形式处理键冲突和指定 Map 类型。常见场景包括 ID 到对象映射、属性提取及分组聚合。使用时需注意空值处理、键重复策略及并行流下的线程安全。性能与传统循环相当,代码更简洁。
菩提31 浏览 引言:从 List 到 Map 的优雅转换
在日常开发中,我们经常遇到这样的场景:有一个对象列表,需要快速通过某个属性查找对象。比如通过城市 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);
}
Map<Integer, City> cityMap = cityList.stream().collect(Collectors.toMap(City::getCityId, Function.identity()));
一、Collectors.toMap 概述
1.1 什么是 Collectors.toMap?
Collectors.toMap 是 Java Stream API 提供的收集器,用于将流中的元素转换为键值对,并收集到一个 Map 中。
1.2 适用场景
| 场景 | 说明 | 示例 |
|---|
| ID-对象映射 | 通过 ID 快速查找对象 | 用户 ID -> 用户对象 |
| ID-属性映射 | 提取对象的某个属性 | 城市 ID -> 城市名称 |
| 分组聚合 | 按某个维度统计 | 销售员 -> 总销售额 |
| 去重转换 | 处理键冲突情况 | 自定义冲突策略 |
二、三种重载方法详解
2.1 两个参数:基础版
public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper
)
特点:
- 最简单的形式
- 默认使用 HashMap
- 键冲突时抛出异常
2.2 三个参数:冲突处理版
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
)
2.3 四个参数:指定 Map 实现版
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 类型(HashMap、TreeMap、LinkedHashMap 等)
- 最大灵活性
| 无重复键 | 可能有重复键 | 需要指定 Map 类型 |
|---|
| Stream 流 | 使用哪个 toMap? | 双参数版本 |
| HashMap | 三参数版本 | 四参数版本 |
| 自定义合并策略 | TreeMap/LinkedHashMap 等 | |
三、实战示例
3.1 准备实体类
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;
}
3.2 示例 1:ID 到名称的映射
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<Integer, String> cityNameMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName
)
);
System.out.println("城市名称映射:" + cityNameMap);
System.out.println("城市 ID 3 的名称:" + cityNameMap.get(3));
}
}
城市名称映射:{1=北京,2=上海,3=广州,4=深圳}
城市 ID 3 的名称:广州
3.3 示例 2:ID 到对象的映射
@Test
public void testIdToObjectMap() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳")
);
Map<Integer, City> cityMap1 = cityList.stream().collect(
Collectors.toMap(
city -> city.getCityId(),
city -> city
)
);
Map<Integer, City> cityMap2 = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
Function.identity()
)
);
City city = cityMap2.get(1);
System.out.println("城市 ID: " + city.getCityId());
System.out.println("城市名称:" + city.getCityName());
}
3.4 示例 3:处理键冲突
@Test
public void testDuplicateKeyHandling() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳"),
new City(4, "天津")
);
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 天津)
保留旧值:深圳
使用新值:天津
合并值:深圳,天津
3.5 示例 4:分组聚合统计
@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}
3.6 示例 5:指定 Map 实现类型
@Test
public void testSpecifyMapType() {
List<City> cityList = Arrays.asList(
new City(2, "上海"),
new City(1, "北京"),
new City(4, "深圳"),
new City(3, "广州")
);
Map<Integer, String> hashMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old
)
);
System.out.println("HashMap (无序): " + hashMap);
Map<Integer, String> linkedHashMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old,
LinkedHashMap::new
)
);
System.out.println("LinkedHashMap (插入顺序): " + linkedHashMap);
Map<Integer, String> treeMap = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old,
TreeMap::new
)
);
System.out.println("TreeMap (键排序): " + treeMap);
Map<Integer, String> concurrentMap = cityList.parallelStream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(old, now) -> old,
ConcurrentHashMap::new
)
);
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=深圳}
四、高级应用技巧
4.1 复杂对象转换
@Test
public void testComplexTransformation() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海")
);
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);
}
@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";
}
}
4.2 条件过滤
@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)
);
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);
}
4.3 多字段组合键
@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"))
);
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;
}
五、注意事项与常见陷阱
5.1 Null 值问题
@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)
);
try {
Map<Integer, String> map = cityList.stream().collect(
Collectors.toMap(City::getCityId, City::getCityName)
);
} catch (NullPointerException e) {
System.out.println("NullPointerException: " + e.getMessage());
}
Map<Integer, String> solution1 = cityList.stream()
.filter(city -> city.getCityName() != null)
.collect(Collectors.toMap(City::getCityId, City::getCityName));
Map<Integer, String> solution2 = cityList.stream().collect(
Collectors.toMap(
City::getCityId,
city -> Optional.ofNullable(city.getCityName()).orElse("未知")
)
);
}
5.2 键重复问题
@Test
public void testDuplicateKeyProblem() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(1, "天津")
);
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
)
);
}
5.3 并行流问题
@Test
public void testParallelStream() {
List<City> cityList = Arrays.asList(
new City(1, "北京"),
new City(2, "上海"),
new City(3, "广州"),
new City(4, "深圳")
);
Map<Integer, String> map = cityList.parallelStream().collect(
Collectors.toMap(
City::getCityId,
City::getCityName,
(v1, v2) -> v1,
ConcurrentHashMap::new
)
);
System.out.println("并行流结果:" + map);
}
六、性能对比
6.1 传统循环 vs Stream
@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;
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");
}
传统循环耗时:35ms
Stream 耗时:28ms
性能差异:7ms
6.2 不同 Map 实现性能
| Map 类型 | 插入 10 万条耗时 | 特点 |
|---|
| HashMap | 28ms | 最快,无序 |
| LinkedHashMap | 32ms | 稍慢,保持顺序 |
| TreeMap | 85ms | 最慢,自动排序 |
| ConcurrentHashMap | 35ms | 线程安全 |
七、最佳实践总结
7.1 使用建议
| 场景 | 推荐写法 | 说明 |
|---|
| 简单 ID-对象映射 | toMap(keyMapper, Function.identity()) | 最常用 |
| ID-属性映射 | toMap(keyMapper, valueMapper) | 提取特定属性 |
| 可能有重复键 | 提供合并函数 | 避免异常 |
| 需要有序 Map | 指定 LinkedHashMap | 保持插入顺序 |
| 需要排序 Map | 指定 TreeMap | 按键排序 |
| 并行流处理 | 指定 ConcurrentHashMap | 线程安全 |
7.2 常见问题检查清单
- value 是否为 null? → 需要过滤或提供默认值
- key 是否可能重复? → 需要提供合并函数
- 是否需要保持顺序? → 考虑 LinkedHashMap
- 是否在并行流中使用? → 考虑 ConcurrentHashMap
- 是否需要排序? → 考虑 TreeMap
7.3 代码模板
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<KeyType, ValueType> orderedMap = list.stream().collect(
Collectors.toMap(
Item::getKey,
Item::getValue,
(v1, v2) -> v1,
LinkedHashMap::new
)
);
总结
| 特性 | 说明 |
|---|
| 核心功能 | 将 List 转换为 Map,实现快速查找 |
| 三个版本 | 2 参数(基础)、3 参数(冲突处理)、4 参数(指定 Map) |
| 常见用途 | ID 映射、属性提取、分组聚合 |
| 注意事项 | null 值处理、键冲突、并发安全 |
| 性能表现 | 与传统循环相当,代码更优雅 |
Collectors.toMap 是 Java Stream 中最实用的收集器之一,掌握它能让你用一行代码搞定过去需要写循环的复杂转换!
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online