Преобразование / группирование значений внутри списка

#java #collections #java-8 #java-stream #groupingby

#java #Коллекции #java-8 #java-поток #группирование

Вопрос:

Учитывая
List<Student> , где у каждого Student есть List<Book> , группируйте по Book List<Student>

Рабочее решение (использует Java 9) У меня уже есть рабочий код, упомянутый ниже

 public class Demo {
    public static void main(String[] args) {

    List<Student> studs = createStudents();

        var data = studs.stream()
                .flatMap(s -> s.getBooks()
                        .stream()
                        .map(b -> Pair.of(b, s))) // <------ is possible without using external class?
                .collect(Collectors.groupingBy(
                        p -> p.getBook(),
                        Collectors.mapping(p -> p.getStud().getFirstName(), Collectors.toList())));
        System.out.println("All books: "   data);
    }

    private static List<Student> createStudents() {
        Book javaCompleteReference = new Book("Java Complete Reference");
        Book ocjp = new Book("Java OCJP");
        Book sql = new Book("SQL");
        Book spring = new Book("SPRING in Action");

        return List.of(
                new Student(1, "A", List.of(javaCompleteReference, spring)),
                new Student(2, "B", List.of(spring, sql, ocjp)),
                new Student(3, "C", List.of(javaCompleteReference, sql))
        );
    }
}
 

Ищете:

  1. Я использовал flatMap и пару промежуточных классов. Возможно ли достичь конечного результата без использования промежуточного преобразования с использованием класса Pair

База кода:

 public class Book {
    private String title;
    private String id;
    private Integer pages;

    public Book(String title, String id, Integer pages) {
        this.title = title;
        this.id = id;
        this.pages = pages;
    }

    public Book(String title) { this.title = title; }

    public String getTitle() { return title; }

    public String getId() { return id; }

    public Integer getPages() { return pages; }

    @Override
    public String toString() { return title; }
}

class Pair {
    Book book;
    Student stud;

    private Pair(Book book, Student stud) {
        this.book = book;
        this.stud = stud;
    }

    static Pair of(Book book, Student stud) { return new Pair(book, stud); }

    public Book getBook() { return book; }

    public Student getStud() { return stud; }
}

public class Student {
    private Integer id;
    private String firstName;
    private List<Book> books;

    public Student(Integer id, String firstName, List<Book> books) {
        this.id = id;
        this.firstName = firstName;
        this.books = books;
    }

    public Integer getId() { return id; }

    public String getFirstName() { return firstName; }

    public List<Book> getBooks() { return books; }
}
 

Вывод: All books: {Java Complete Reference=[A, C], SQL=[B, C], SPRING in Action=[A, B], Java OCJP=[B]}

Комментарии:

1. Пожалуйста, прочитайте: могу ли я задать только один вопрос за сообщение? — » является ли попытка решения элегантной » — на основе мнений. — » есть лучший способ добиться результата». — Какова ваша метрика для улучшения ? Этот вопрос неясен.

2. ОК. Сократил формулировку проблемы до одного вопроса

3. Я не думаю, что это возможно, поскольку мы можем читать каждый Student только один раз, поэтому нам нужно создать некоторый временный ресурс ( Pair экземпляры) для повторного чтения. Почему вы думаете, что решение без map шага » лучше «? Или, скорее: какую актуальную проблему вы пытаетесь решить?

4. Я думаю, что два вложенных forEach более удобочитаемы. тем не менее, вы тоже можете сделать это. studs.stream().collect(HashMap::new, (m, s) -> s.getBooks().forEach(b -> m.merge(b, new ArrayList<>(Collections.singletonList(s.getFirstName())), (l1, l2) -> { l1.addAll(l2);return l1; })), HashMap::putAll);

5. обсуждения добавили ценность моим знаниям, однако я думаю, что моя существующая реализация более понятна, чем другие умные предложения.

Ответ №1:

Это невозможно с помощью Stream API, если вам необходимо сохранить как информацию об имени учащегося, так и о каждой книге в контексте, чтобы сформировать выходную карту. Плоское отображение в один элемент (пару) — это единственный путь.

Доказательством и альтернативным (я думаю, не лучшим) решением является использование комбинации Collectors.flatMapping и Collectors.toMap , где вы определяете все keyMapper , valueMapper и mergeFunction . Вы можете ясно видеть, как в Collectors.grouping , вам нужна комбинация имен всех студентов и каждой книги для каждого из них.

 var data = studs.stream().collect(Collectors.flatMapping(
    student -> student.getBooks()
                      .stream()
                      .map(book -> new AbstractMap.SimpleEntry<>(
                              student.getFirstName(), book)),
    Collectors.toMap(
        AbstractMap.SimpleEntry::getValue,
        entry -> new ArrayList<>(List.of(entry.getKey())),
        (leftList, rightList) -> { leftList.addAll(rightList); return leftList; })));
 

Ответ №2:

возможно ли без использования внешнего класса?

Да, это возможно даже в более ранних версиях java. Просто используйте AbstractMap.SimpleEntry<K, V> вместо:

 .map(b -> new AbstractMap.SimpleEntry<>(b, s)))
 

Затем вы можете получить доступ к значениям, вызвав getKey() и getValue() .

Комментарии:

1. Чем это отличается от использования a Pair ? Можем ли мы на самом деле создать a Map с коллектором, повторно используя SimpleEntry s?

2. @Turing85 «Чем это отличается … ?». Ну, по крайней мере, вам не нужно определять другой класс. Что касается повторного использования SimpleEntry, не уверен. Возможно, мы можем.