3

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

ETO
  • 6,970
  • 1
  • 20
  • 37
Ajeetkumar
  • 1,271
  • 7
  • 16
  • 34
  • Please read: [Can I ask only one question per post?](https://meta.stackexchange.com/questions/222735/can-i-ask-only-one-question-per-post) --- "*is the solution attempted elegant*" - Opinion-based. --- "*there is better way to achieve the result.*" - What is your metric for *better*? This question in unclear. – Turing85 Feb 28 '21 at 13:59
  • Ok. Reduced the problem statement to one question – Ajeetkumar Feb 28 '21 at 14:01
  • I do not think that it is possible since we can read every `Student` only once, hence we have to create some temporary ressource (the `Pair` instances) for re-read. Why do you think that a solution without a `map`-step is "*better*"? Or rather: what actual problem are you trying to solve? – Turing85 Feb 28 '21 at 14:18
  • I think two nested `forEach` is more readable. nevertheless you can do like this too. `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);` – Hadi J Feb 28 '21 at 14:49
  • @Turing85 It felt like unwanted route to me, was looking for alternative approach. The problem to solve is achieve the transformed result but flatMap block looks little uneasy to me. – Ajeetkumar Feb 28 '21 at 15:13
  • @HadiJ Thanks for alternative. but that looks more jumbled to be, wouldn't want to perform a war while doing terminal operation – Ajeetkumar Feb 28 '21 at 15:17
  • @Ajeetkumar: Have you found your solution? – Nikolas Charalambidis Mar 05 '21 at 11:05
  • 1
    discussions added value to my knowledge, however I think my existing implementation is more clear than other clever suggestions. – Ajeetkumar Mar 05 '21 at 11:07
  • I think so. Your current solution is more than sufficient. – Nikolas Charalambidis Mar 05 '21 at 12:02

2 Answers2

0

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().

ETO
  • 6,970
  • 1
  • 20
  • 37
  • How is this different from using a `Pair`? Can we actually construct a `Map` with a collector, resusing the `SimpleEntry`s? – Turing85 Feb 28 '21 at 14:19
  • @Turing85 "How is this different... ?". Well, at least you don't have to define another class. Regarding the reusing SimpleEntry's, not sure. Perhaps we can. – ETO Feb 28 '21 at 14:29
0

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; })));
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183