本文将详细介绍如何使用Java Stream API将一个对象列表(如List
在Java开发中,我们经常需要对集合数据进行转换和聚合。一个常见的需求是将一个对象列表根据某个属性进行分组,并将分组结果存储到Map中,其中Map的键是分组依据的属性值,而值是所有符合该属性的对象列表。例如,我们可能有一个Child对象的列表,每个Child对象都关联一个Parent,我们希望将所有Child对象按照其parent_id进行分组。
场景描述与常见误区
假设我们有以下数据结构:
// 父级实体 class Parent { private Long parentId; private String projectDesc; public Parent(Long parentId, String projectDesc) { this.parentId = parentId; this.projectDesc = projectDesc; } public Long getParentId() { return parentId; } // Getters and other methods } // 子级实体 class Child { private Long childId; private Parent parent; // 子级关联父级对象 private String code; public Child(Long childId, Parent parent, String code) { this.childId = childId; this.parent = parent; this.code = code; } public Long getChildId() { return childId; } public Parent getParent() { return parent; } public String getCode() { return code; } @Override public String toString() { return "Child{" + "childId=" + childId + ", parentId=" + (parent != null ? parent.getParentId() : "null") + ", code='" + code + ''' + '}'; } }
现在,我们有一个List
Parent parent1 = new Parent(1L, "One"); Parent parent2 = new Parent(2L, "Two"); List<Child> childEntityList = Arrays.asList( new Child(1L, parent1, "code1"), new Child(2L, parent1, "code2"), new Child(3L, parent1, "code3"), new Child(4L, parent2, "code4"), new Child(5L, parent2, "code5") );
我们的目标是将其转换为Map
立即学习“Java免费学习笔记(深入)”;
初学者在尝试使用Java Stream API实现此功能时,可能会倾向于使用Collectors.toMap(),如下所示:
import java.util.List; import java.util.Map; import java.util.stream.Collectors; // ... (Child and Parent class definitions as above) // 错误示例:使用 toMap() // Map<Long, List<Child>> childEntityMap = childEntityList.stream() // .collect(Collectors.toMap( // childEntity -> childEntity.getParent().getParentId(), // childEntity -> childEntity // ));
上述代码在运行时会抛出IllegalStateException: Duplicate key …异常。这是因为Collectors.toMap()默认期望每个键都是唯一的。当多个Child对象拥有相同的parent_id时(例如,child1、child2和child3都属于parent1),toMap()无法处理同一个键关联多个值的情况,从而导致异常。
正确的解决方案:使用 Collectors.groupingBy()
为了解决一对多(one-to-many)的映射问题,Java Stream API提供了专门的收集器:Collectors.groupingBy()。这个收集器正是为这种分组场景设计的。
Collectors.groupingBy()的基本用法是接收一个分类函数(classifier function),该函数用于从流中的每个元素中提取出作为Map键的值。默认情况下,所有被分类到同一个键的元素将被收集到一个List中作为Map的值。
以下是使用Collectors.groupingBy()正确实现上述需求的示例代码:
import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.Arrays; // For Arrays.asList public class StreamGroupingExample { // ... (Child and Parent class definitions as above) public static void main(String[] args) { Parent parent1 = new Parent(1L, "One"); Parent parent2 = new Parent(2L, "Two"); List<Child> childEntityList = Arrays.asList( new Child(1L, parent1, "code1"), new Child(2L, parent1, "code2"), new Child(3L, parent1, "code3"), new Child(4L, parent2, "code4"), new Child(5L, parent2, "code5") ); // 正确示例:使用 groupingBy() Map<Long, List<Child>> childEntityMap = childEntityList.stream() .collect(Collectors.groupingBy( childEntity -> childEntity.getParent().getParentId() )); // 打印结果以验证 childEntityMap.forEach((parentId, children) -> { System.out.println("Parent ID: " + parentId); children.forEach(child -> System.out.println(" " + child)); }); /* 预期输出: Parent ID: 1 Child{childId=1, parentId=1, code='code1'} Child{childId=2, parentId=1, code='code2'} Child{childId=3, parentId=1, code='code3'} Parent ID: 2 Child{childId=4, parentId=2, code='code4'} Child{childId=5, parentId=2, code='code5'} */ } }
在上述代码中:
- childEntityList.stream() 创建了一个Child对象的流。
- collect(Collectors.groupingBy(…)) 是核心操作。
- childEntity -> childEntity.getParent().getParentId() 是分类函数,它从每个Child对象中提取出parent_id作为Map的键。
- groupingBy()默认会将所有具有相同parent_id的Child对象收集到一个List
中,作为该parent_id对应的值。
注意事项与总结
- 选择正确的收集器: 当你需要将流中的元素按照某个属性进行分组,并且每个键可能对应多个元素时,始终使用Collectors.groupingBy()。如果你确定每个键都是唯一的,并且希望将流转换为Map
,那么Collectors.toMap()是合适的选择,但需注意处理键冲突的策略(例如使用mergeFunction)。 - 默认行为: Collectors.groupingBy()的单参数版本默认将值收集到ArrayList中。如果你需要不同的集合类型(如HashSet),或者对收集到的值进行进一步的聚合,可以使用groupingBy的其他重载版本,它们允许你指定一个下游收集器(downstream collector)。
- 可读性与简洁性: Java Stream API,尤其是Collectors类,提供了强大且富有表现力的方法来处理集合数据。理解并正确使用这些方法可以大大提高代码的可读性和简洁性。
通过本文的介绍,您应该已经掌握了如何使用Collectors.groupingBy()来高效地将对象列表按照指定属性分组到Map中,从而避免了toMap()在处理一对多关系时可能遇到的问题。掌握这一技巧对于编写高效、健壮的Java Stream代码至关重要。