繁体   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