[英]Flatten a map after Collectors.groupingBy in java
我有学生名单。 我想返回具有课程的对象列表StudentResponse课程以及课程的学生列表。 所以我可以写一下给我一张地图
Map<String, List<Student>> studentsMap = students.stream().
.collect(Collectors.groupingBy(Student::getCourse,
Collectors.mapping(s -> s, Collectors.toList()
)));
现在我必须再次遍历地图以创建StudentResponse
类的对象列表,其中包含课程和列表:
class StudentResponse {
String course;
Student student;
// getter and setter
}
有没有办法结合这两个迭代?
可能有点矫枉过正,但这是一个有趣的练习:)你可以实现自己的收藏家:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class StudentResponseCollector implements Collector<Student, Map<String, List<Student>>, List<StudentResponse>> {
@Override
public Supplier<Map<String, List<Student>>> supplier() {
return () -> new ConcurrentHashMap<>();
}
@Override
public BiConsumer<Map<String, List<Student>>, Student> accumulator() {
return (store, student) -> store.merge(student.getCourse(),
new ArrayList<>(Arrays.asList(student)), combineLists());
}
@Override
public BinaryOperator<Map<String, List<Student>>> combiner() {
return (x, y) -> {
x.forEach((k, v) -> y.merge(k, v, combineLists()));
return y;
};
}
private <T> BiFunction<List<T>, List<T>, List<T>> combineLists() {
return (students, students2) -> {
students2.addAll(students);
return students2;
};
}
@Override
public Function<Map<String, List<Student>>, List<StudentResponse>> finisher() {
return (store) -> store
.keySet()
.stream()
.map(course -> new StudentResponse(course, store.get(course)))
.collect(Collectors.toList());
}
@Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
鉴于学生和学生的响应:
public class Student {
private String name;
private String course;
public Student(String name, String course) {
this.name = name;
this.course = course;
}
public String getName() {
return name;
}
public String getCourse() {
return course;
}
public String toString() {
return name + ", " + course;
}
}
public class StudentResponse {
private String course;
private List<Student> studentList;
public StudentResponse(String course, List<Student> studentList) {
this.course = course;
this.studentList = studentList;
}
public String getCourse() {
return course;
}
public List<Student> getStudentList() {
return studentList;
}
public String toString() {
return course + ", " + studentList.toString();
}
}
您收集StudentResponses的代码现在可以非常简短而优雅;)
public class StudentResponseCollectorTest {
@Test
public void test() {
Student student1 = new Student("Student1", "foo");
Student student2 = new Student("Student2", "foo");
Student student3 = new Student("Student3", "bar");
List<Student> studentList = Arrays.asList(student1, student2, student3);
List<StudentResponse> studentResponseList = studentList
.stream()
.collect(new StudentResponseCollector());
assertEquals(2, studentResponseList.size());
}
}
不完全是你所问的,但这是一种实现你想要的紧凑方式,只是为了完整性:
Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
s.getCourse(),
k -> new StudentResponse(s.getCourse()))
.getStudents().add(s));
这假设StudentResponse
有一个构造函数,它接受课程作为参数和学生列表的getter,并且这个列表是可变的(即ArrayList
),以便我们可以将当前学生添加到它。
虽然上述方法有效,但它明显违反了基本的OO原则,即封装。 如果你对此感到满意,那么你已经完成了。 如果您想要尊重封装,那么您可以向StudentResponse
添加一个方法来添加Student
实例:
public void addStudent(Student s) {
students.add(s);
}
然后,解决方案将成为:
Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
s.getCourse(),
k -> new StudentResponse(s.getCourse()))
.addStudent(s));
这个解决方案明显优于前一个解决方案,可以避免严重的代码审查员拒绝。
两种解决方案都依赖于Map.computeIfAbsent
,它返回所提供课程的StudentResponse
(如果在地图中存在该课程的条目),或创建并返回以课程为参数构建的StudentResponse
实例。 然后,学生被添加到返回的StudentResponse
的学生的内部列表中。
最后,您的StudentResponse
实例位于地图值中:
Collection<StudentResponse> result = map.values();
如果您需要List
而不是Collection
:
List<StudentResponse> result = new ArrayList<>(map.values());
注意:我使用LinkedHashMap
而不是HashMap
来保留插入顺序,即原始列表中学生的顺序。 如果您没有这样的要求,只需使用HashMap
。
首先,您的下游收集器( mapping
)是冗余的,因此您可以通过在没有下游收集器的情况下使用groupingBy
重载来简化代码。
给定List<T>
作为源,在使用groupingBy
重载之后单独使用分类器,结果映射是Map<K, List<T>>
因此可以避免映射操作。
至于你的问题,你可以使用collectingAndThen
:
students.stream()
.collect(collectingAndThen(groupingBy(Student::getCourse),
m -> m.entrySet()
.stream()
.map(a -> new StudentResponse(a.getKey(), a.getValue()))
.collect(Collectors.toList())));
collectingAndThen
基本上:
调整收集器以执行其他完成转换。
只需迭代条目集并将每个条目映射到StudentResponse
:
List<StudentResponse> responses = studentsMap.entrySet()
.stream()
.map(e -> new StudentResponse(e.getKey(), e.getValue()))
.collect(Collectors.toList());
这可以使用jOOλ库及其Seq.grouped
方法以非常简洁的方式完成:
List<StudentResponse> responses = Seq.seq(students)
.grouped(Student::getCourse, Collectors.toList())
.map(Tuple.function(StudentResponse::new))
.toList();
它假定StudentResponse
具有构造函数StudentResponse(String course, List<Student> students)
,并使用以下Tuple.function
重载转发到此构造函数。
从我的其他答案以及shmosel的答案中可以看出,您最终需要调用studentsMap.entrySet()
将结果映射中的每个Entry<String, List<String>>
映射到StudentResponse
对象。
您可以采用的另一种方法是toMap
方式; 即
Collection<StudentResponse> result = students.stream()
.collect(toMap(Student::getCourse,
v -> new StudentResponse(v.getCourse(),
new ArrayList<>(singletonList(v))),
StudentResponse::merge)).values();
这基本上将Student
对象按其课程( Student::getCourse
) groupingBy
就像groupingBy
收集器一样; 然后在valueMapper
函数中从Student
映射到StudentResponse
,最后在merge
函数中,在键冲突的情况下使用StudentResponse::merge
。
以上对StudentResponse
类的依赖至少包含以下字段,构造函数和方法:
class StudentResponse {
StudentResponse(String course, List<Student> students) {
this.course = course;
this.students = students;
}
private List<Student> getStudents() { return students; }
StudentResponse merge(StudentResponse another){
this.students.addAll(another.getStudents());
// maybe some addition merging logic in the future ...
return this;
}
private String course;
private List<Student> students;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.