List
到 Map
的优雅转换 大家好! 今天我们来聊聊 Java 8 中一个非常常见的操作:使用 Stream API 将 List
转换为 Map
。 具体来说,我们将深入分析以下代码片段:
Map<Integer, InviteCode> inviteCodeMap = inviteCodes.stream()
.collect(Collectors.toMap(InviteCode::getId, ic -> ic));
这段代码看似简单,但背后涉及了 Stream API、方法引用、Lambda 表达式以及 Collectors.toMap
的强大功能。 我们将从代码的背景开始,逐步拆解它的实现原理,探讨使用场景、优势和优化方法,最后通过一个实际案例展示它的应用。为了更直观地理解整个过程,我们还会插入一个 Mermaid 流程图!
准备好了吗?让我们开始吧!
List
转换为 Map
?在 Java 开发中,我们经常需要处理集合数据。例如,在一个邀请码系统中,我们有一个 List
,其中 InviteCode
是一个实体类,包含 id
、inviteCode
、inviteLevel
和 createdBy
等字段:
public class InviteCode {
private Integer id;
private String inviteCode;
private Integer inviteLevel;
private Integer createdBy;
// Getters and Setters
public Integer getId() {
return id;
}
public String getInviteCode() {
return inviteCode;
}
public Integer getInviteLevel() {
return inviteLevel;
}
public Integer getCreatedBy() {
return createdBy;
}
}
假设我们有一个 List
,包含 adminId = 7
的所有邀请码记录:
id | admin_id | created_by | invite_code | invite_level |
---|---|---|---|---|
20 | 7 | NULL | ****** | 0 |
21 | 7 | 20 | 263113 | 1 |
22 | 7 | 20 | 704358 | 1 |
23 | 7 | 20 | 982868 | 1 |
24 | 7 | NULL | ****** | 0 |
25 | 7 | 24 | ****** | 1 |
26 | 7 | 25 | ****** | 2 |
27 | 7 | 26 | 991476 | 3 |
我们的目标是构建一个以 adminId
为根的邀请码层级树。为了高效地查找某个 id
对应的 InviteCode
对象,我们需要将 List
转换为 Map
,其中:
InviteCode
的 id
(Integer
类型)。InviteCode
对象本身。这就是以下代码的作用:
Map<Integer, InviteCode> inviteCodeMap = inviteCodes.stream()
.collect(Collectors.toMap(InviteCode::getId, ic -> ic));
让我们逐步拆解这段代码,弄清楚它是如何工作的!
inviteCodes.stream()
inviteCodes
:是一个 List
,包含 adminId = 7
的 8 条记录(id = 20, 21, ..., 27
)。stream()
:将 List
转换为一个 Stream
。
Stream
是 Java 8 引入的流式 API,允许你以声明式的方式处理集合数据(例如映射、过滤、归约等)。结果:inviteCodes.stream()
生成了一个 Stream
,包含 8 个 InviteCode
对象。
.collect(Collectors.toMap(...))
collect
:是 Stream
的终止操作,用于将流中的元素收集到一个结果容器中(例如 List
、Set
或 Map
)。Collectors.toMap
:是一个收集器(Collector),专门用于将流中的元素收集到一个 Map
中。Collectors.toMap
的方法签名public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper
)
keyMapper
:一个函数,用于从流中的每个元素提取 Map
的键(Key)。valueMapper
:一个函数,用于从流中的每个元素提取 Map
的值(Value)。Map
。在我们的代码中:
T
是 InviteCode
(流中的元素类型)。K
是 Integer
(键的类型)。U
是 InviteCode
(值的类型)。InviteCode::getId
InviteCode::getId
:这是一个方法引用(Method Reference),等价于 Lambda 表达式 ic -> ic.getId()
。InviteCode
对象中提取 id
字段,作为 Map
的键。Function
,将 InviteCode
映射为 Integer
。示例:
InviteCode
对象的 id
是 20
,InviteCode::getId
会返回 20
。ic -> ic
ic -> ic
:这是一个 Lambda 表达式,表示一个简单的映射函数。InviteCode
对象本身作为 Map
的值。Function
,将 InviteCode
映射为它自己。示例:
InviteCode
对象是 ic
(id = 20
),ic -> ic
直接返回这个 ic
对象。Collectors.toMap(InviteCode::getId, ic -> ic)
:
Stream
中的每个 InviteCode
对象:
InviteCode::getId
提取 id
作为键。ic -> ic
提取整个 InviteCode
对象作为值。Map
中。结果:
inviteCodeMap
是一个 Map
,其中:
inviteCodeMap.get(20)
:InviteCode(id=20, inviteCode="******", ...)
。inviteCodeMap.get(21)
:InviteCode(id=21, inviteCode="263113", ...)
。inviteCodeMap.get(27)
:InviteCode(id=27, inviteCode="991476", ...)
。为了更直观地理解 List
到 Map
的转换过程,我们使用 Mermaid 流程图来展示:
List
开始,转换为 Stream
。InviteCode
对象:
InviteCode::getId
提取键(id
)。ic -> ic
提取值(InviteCode
对象)。Map
中。inviteCodeMap
?在邀请码系统中,我们的目标是构建一个以 adminId
为根的层级树。为了高效地查找某个 id
对应的 InviteCode
对象,我们需要将 List
转换为 Map
。
// 找到所有根节点(createdBy = NULL)
List<InviteCode> roots = inviteCodes.stream()
.filter(ic -> ic.getCreatedBy() == null)
.collect(Collectors.toList());
// 为每个根节点构建树形结构
List<InviteCodeTreeDTO> trees = new ArrayList<>();
for (InviteCode root : roots) {
InviteCodeTreeDTO tree = buildTree(root, inviteCodeMap, new HashSet<>());
trees.add(tree);
}
在 buildTree
方法中,需要根据 createdBy
查找子节点:
private InviteCodeTreeDTO buildTree(InviteCode root, Map<Integer, InviteCode> inviteCodeMap, Set<Integer> visited) {
if (!visited.add(root.getId())) {
throw new IllegalStateException("Detected a cycle in invite code hierarchy at ID: " + root.getId());
}
InviteCodeTreeDTO node = new InviteCodeTreeDTO();
node.setId(root.getId());
node.setInviteCode(root.getInviteCode());
node.setInviteLevel(root.getInviteLevel());
node.setChildren(new ArrayList<>());
// 查找所有子节点(createdBy = root.id)
List<InviteCode> children = inviteCodeMap.values().stream()
.filter(ic -> Objects.equals(ic.getCreatedBy(), root.getId()))
.collect(Collectors.toList());
for (InviteCode child : children) {
InviteCodeTreeDTO childNode = buildTree(child, inviteCodeMap, new HashSet<>(visited));
node.getChildren().add(childNode);
}
return node;
}
Map
?
inviteCodes
查找子节点(createdBy = root.id
),时间复杂度是 O(n)
。inviteCodeMap
,可以通过 id
直接查找 InviteCode
对象,时间复杂度是 O(1)
(尽管当前 children
查找仍需优化)。for
循环更简洁。Map
:Map<Integer, InviteCode> inviteCodeMap = new HashMap<>();
for (InviteCode ic : inviteCodes) {
inviteCodeMap.put(ic.getId(), ic);
}
inviteLevel > 0
的 InviteCode
:Map<Integer, InviteCode> inviteCodeMap = inviteCodes.stream()
.filter(ic -> ic.getInviteLevel() > 0)
.collect(Collectors.toMap(InviteCode::getId, ic -> ic));
parallelStream()
),在大规模数据下可以提高性能:Map<Integer, InviteCode> inviteCodeMap = inviteCodes.parallelStream()
.collect(Collectors.toMap(InviteCode::getId, ic -> ic));
当前 buildTree
方法中,查找子节点的方式可以通过 inviteCodeMap
进一步优化:
// 预先构建 createdBy 到子节点的映射
Map<Integer, List<InviteCode>> childrenMap = inviteCodes.stream()
.filter(ic -> ic.getCreatedBy() != null)
.collect(Collectors.groupingBy(InviteCode::getCreatedBy));
// 修改 buildTree 方法
private InviteCodeTreeDTO buildTree(InviteCode root, Map<Integer, InviteCode> inviteCodeMap, Map<Integer, List<InviteCode>> childrenMap, Set<Integer> visited) {
if (!visited.add(root.getId())) {
throw new IllegalStateException("Detected a cycle in invite code hierarchy at ID: " + root.getId());
}
InviteCodeTreeDTO node = new InviteCodeTreeDTO();
node.setId(root.getId());
node.setInviteCode(root.getInviteCode());
node.setInviteLevel(root.getInviteLevel());
node.setChildren(new ArrayList<>());
// 查找所有子节点(createdBy = root.id)
List<InviteCode> children = childrenMap.getOrDefault(root.getId(), Collections.emptyList());
for (InviteCode child : children) {
InviteCodeTreeDTO childNode = buildTree(child, inviteCodeMap, childrenMap, new HashSet<>(visited));
node.getChildren().add(childNode);
}
return node;
}
childrenMap
,可以以 O(1)
的时间复杂度找到某个 id
的所有子节点。Collectors.toMap
默认情况下,如果有重复的键(id
),会抛出 IllegalStateException: Duplicate key
。在我们的场景中,id
是主键,应该不会有重复,但为了安全起见,可以指定合并策略:
Map<Integer, InviteCode> inviteCodeMap = inviteCodes.stream()
.collect(Collectors.toMap(
InviteCode::getId,
ic -> ic,
(existing, replacement) -> existing // 如果有重复的 id,保留第一个
));
id
重复,保留第一个 InviteCode
对象。以下是完整的 InviteCodeService
实现,展示了如何使用 inviteCodeMap
构建层级树:
public class InviteCodeService {
private final InviteCodeRepository inviteCodeRepository;
public InviteCodeService(InviteCodeRepository inviteCodeRepository) {
this.inviteCodeRepository = inviteCodeRepository;
}
public AdminInviteCodeTreeDTO getAdminInviteCodeTree(Integer adminId) {
List<InviteCode> inviteCodes = inviteCodeRepository.findByAdminId(adminId);
if (inviteCodes.isEmpty()) {
AdminInviteCodeTreeDTO result = new AdminInviteCodeTreeDTO();
result.setAdminId(adminId);
result.setChildren(Collections.emptyList());
return result;
}
// 将 List 转换为 Map
Map<Integer, InviteCode> inviteCodeMap = inviteCodes.stream()
.collect(Collectors.toMap(InviteCode::getId, ic -> ic));
// 预构建 createdBy 到子节点的映射
Map<Integer, List<InviteCode>> childrenMap = inviteCodes.stream()
.filter(ic -> ic.getCreatedBy() != null)
.collect(Collectors.groupingBy(InviteCode::getCreatedBy));
// 找到所有根节点(createdBy = NULL)
List<InviteCode> roots = inviteCodes.stream()
.filter(ic -> ic.getCreatedBy() == null)
.collect(Collectors.toList());
// 为每个根节点构建树形结构
List<InviteCodeTreeDTO> trees = new ArrayList<>();
for (InviteCode root : roots) {
InviteCodeTreeDTO tree = buildTree(root, inviteCodeMap, childrenMap, new HashSet<>());
trees.add(tree);
}
AdminInviteCodeTreeDTO result = new AdminInviteCodeTreeDTO();
result.setAdminId(adminId);
result.setChildren(trees);
return result;
}
private InviteCodeTreeDTO buildTree(InviteCode root, Map<Integer, InviteCode> inviteCodeMap, Map<Integer, List<InviteCode>> childrenMap, Set<Integer> visited) {
if (!visited.add(root.getId())) {
throw new IllegalStateException("Detected a cycle in invite code hierarchy at ID: " + root.getId());
}
InviteCodeTreeDTO node = new InviteCodeTreeDTO();
node.setId(root.getId());
node.setInviteCode(root.getInviteCode());
node.setInviteLevel(root.getInviteLevel());
node.setChildren(new ArrayList<>());
List<InviteCode> children = childrenMap.getOrDefault(root.getId(), Collections.emptyList());
for (InviteCode child : children) {
InviteCodeTreeDTO childNode = buildTree(child, inviteCodeMap, childrenMap, new HashSet<>(visited));
node.getChildren().add(childNode);
}
return node;
}
}
通过 Stream API 和 Collectors.toMap
,我们可以轻松地将 List
转换为 Map
,为后续的层级树构建提供了高效的数据结构。
inviteCodes.stream().collect(Collectors.toMap(InviteCode::getId, ic -> ic))
将列表转换为映射。childrenMap
提高子节点查找效率,处理键冲突。希望这篇博客对你理解 Stream API 和 Collectors.toMap
有所帮助! 如果你有其他问题,欢迎留言讨论!
参考:Java 官方文档、Collectors
源码。点赞和分享哦!