簡體   English   中英

在Collectors.groupingBy之后將地圖展平

[英]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::getCoursegroupingBy就像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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM