[英]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.