在Java中的Collectors.groupingBy之后展平地图

时间:2018-11-29 22:34:59

标签: java collections java-stream

我有学生名单。 我想返回具有该课程的对象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
}

有没有办法组合这两个迭代?

6 个答案:

答案 0 :(得分:3)

可能是过度矫kill,但这是一个有趣的练习:)您可以实现自己的收集器:

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();
    }
}

您用来收集StudentRespons的代码现在可能非常简短;)

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());
    }
}

答案 1 :(得分:3)

并非完全是您的要求,但这是一种完成您想要的内容的紧凑方法,只是为了保持完整性:

Map<String, StudentResponse> map = new LinkedHashMap<>();
students.forEach(s -> map.computeIfAbsent(
        s.getCourse(), 
        k -> new StudentResponse(s.getCourse()))
    .getStudents().add(s));

这里假设StudentResponse有一个构造函数,该构造函数接受该课程作为参数,并且是学生列表的获取器,并且该列表是可变的(即ArrayList),以便我们可以添加当前学生

虽然上述方法有效,但显然违反了基本的面向对象原理,即封装。如果您对此表示满意,那么您就完成了。如果要遵守封装,则可以向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

答案 2 :(得分:2)

首先,您的下游收集器(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基本上是:

  

适应收集器以执行其他整理转换。

答案 3 :(得分:2)

只需遍历条目集并将每个条目映射到StudentResponse

List<StudentResponse> responses = studentsMap.entrySet()
        .stream()
        .map(e -> new StudentResponse(e.getKey(), e.getValue()))
        .collect(Collectors.toList());

答案 4 :(得分:2)

这可以使用jOOλ库及其Seq.grouped方法以非常简洁的方式完成:

carImg = pygame.image.load("racecar.png")

假定List<StudentResponse> responses = Seq.seq(students) .grouped(Student::getCourse, Collectors.toList()) .map(Tuple.function(StudentResponse::new)) .toList(); 具有构造函数StudentResponse,并使用以下Tuple.function重载转发到该构造函数。

答案 5 :(得分:1)

my other answershmosel's answer都可以看到,最终您将需要调用studentsMap.entrySet()将结果映射中的每个Entry<String, List<String>>映射到{{1 }}对象。

您可以采用的另一种方法是toMap方式;即

StudentResponse

这实际上与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函数中将valueMapper映射到Student,最后在StudentResponse函数中在发生键冲突的情况下使用merge

以上内容与StudentResponse::merge类有关,该类至少具有以下字段,构造函数和方法:

StudentResponse