Transforming/GroupingBy values inside List

Viewed 111

Given List<Student> where each Student has List<Book>, group by Book for List<Student>

Working Solution (Uses Java 9) I already have a working code mentioned below

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

Looking for:

  1. I have used flatMap and an intermediate class Pair. Is it possible to achieve the final result without using intermediate transformation using Pair class

Code Base:

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

Output: All books: {Java Complete Reference=[A, C], SQL=[B, C], SPRING in Action=[A, B], Java OCJP=[B]}

2 Answers

is possible without using external class?

Yes, it is possible even in earlier versions of java. Just use AbstractMap.SimpleEntry<K, V> instead:

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

Then you can access the values by calling getKey() and getValue().

This is not possible using the Stream API as long as you need to keep both information about the Student's name and each Book in the context in order to form an output map. Flatmapping into a single element (pair) is the only wat to go.

A proof and an alternative (I guess not better) solution is using a combination of Collectors.flatMapping and Collectors.toMap where you define all the keyMapper, valueMapper and mergeFunction. You can clearly see as in the Collectors.grouping, you need the combination of all the Student's names and each Book relationship for each one.

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