【问题标题】:Flatten a map after Collectors.groupingBy in java在 Java 中的 Collectors.groupingBy 之后展平地图
【发布时间】:2018-11-29 22:34:58
【问题描述】:

我有学生名单。 我想返回包含课程的 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
}

有没有办法将这两个迭代结合起来?

【问题讨论】:

  • 或者像这样Map&lt;String,StudentResponse&gt; map = new HashMap&lt;&gt;(); students.forEach(student -&gt; { List&lt;Student&gt; value = new ArrayList&lt;&gt;(); value.add(student); map.merge(student.getCourse(), new StudentResponse(student.getCourse(), value), (sr1, sr2) -&gt; { sr1.getStudentList().addAll(sr2.getStudentList()); return sr1; }); });然后使用map.values();

标签: java collections java-stream


【解决方案1】:

不完全是您所要求的,但这里有一个紧凑的方式来完成您想要的,只是为了完整性:

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

【讨论】:

    【解决方案2】:

    可能有点矫枉过正,但这是一个有趣的练习 :) 你可以实现自己的收集器:

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

        首先,您的下游收集器 (mapping) 是多余的,因此您可以通过使用 groupingBy 重载而无需下游收集器来简化代码。

        给定List&lt;T&gt;作为源,使用groupingBy重载单独使用分类器后,结果映射为Map&lt;K, List&lt;T&gt;&gt;,因此可以避免映射操作。

        至于你的问题,你可以使用collectingAndThen

        students.stream()
                .collect(collectingAndThen(groupingBy(Student::getCourse), 
                           m -> m.entrySet()
                                .stream()
                                .map(a -> new StudentResponse(a.getKey(), a.getValue()))
                                .collect(Collectors.toList())));
        

        collectingAndThen 基本上:

        调整收集器以执行额外的整理转换。

        【讨论】:

        • collectingAndThen() 当您想将结果传递给另一个函数时可以更简洁。 IMO,将它用于链式方法是没有意义的。
        • @shmosel 主观问题,所以我不想就此发表我的看法。
        【解决方案5】:

        这可以使用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&lt;Student&gt; students),并使用下面的Tuple.function 重载转发给这个构造函数。

        【讨论】:

          【解决方案6】:

          my other answershmosel's answer 可以看出,您最终需要调用studentsMap.entrySet() 以将结果映射中的每个Entry&lt;String, List&lt;String&gt;&gt; 映射到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 收集器进行分组;然后在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;
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2014-10-26
            • 1970-01-01
            • 1970-01-01
            • 2019-08-26
            • 1970-01-01
            • 2017-08-22
            • 1970-01-01
            相关资源
            最近更新 更多