Свести карту после Collectors.groupingBy в Java - PullRequest
0 голосов
/ 30 ноября 2018

У меня есть список студентов.Я хочу вернуть список объектов 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 голосов
/ 30 ноября 2018

Это можно сделать очень кратко, используя библиотеку 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 перегрузку.

0 голосов
/ 30 ноября 2018

Не совсем то, что вы просили, но вот компактный способ выполнить то, что вы хотите, просто для полноты:

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.

0 голосов
/ 30 ноября 2018

Возможно, это излишне, но это было забавное упражнение :) Вы могли бы реализовать свой собственный Collector:

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

С учетом Student и StudentResponse:

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

Ваш код, где вы собираете свойОтветы учеников теперь могут быть очень короткими и элегантными;)

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());
    }
}
0 голосов
/ 30 ноября 2018

Как вы можете видеть из моего другого ответа , а также ответа 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;затем в функции 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;
}
0 голосов
/ 30 ноября 2018

Просто переберите набор записей и сопоставьте каждую запись с StudentResponse:

List<StudentResponse> responses = studentsMap.entrySet()
        .stream()
        .map(e -> new StudentResponse(e.getKey(), e.getValue()))
        .collect(Collectors.toList());
0 голосов
/ 30 ноября 2018

Во-первых, ваш нисходящий коллектор (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 в основном:

Адаптирует коллектор для выполнения дополнительного завершающего преобразования.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...