Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rewrite for-each loop into stream?

I have a data structure named "Book" that consists of the following fields:

public final class Book {
    private final String title;
    private final BookType bookType;
    private final List<Author> authors;
}

My goal is to derive a Map<Author, List<BookType>> from a List<Book>using Stream API.

To achieve it, at first, I've made a for-each loop to clarify the steps of the solution and after I've rewritten it into streams based approach step-by-step:

Map<Author, List<BookType>> authorListBookType = new HashMap<>();
books.stream().forEach(b -> b.getAuthors().stream().forEach(e -> {
     if (authorListBookType.containsKey(e)) {
        authorListBookType.get(e).add(b.getBookType());
     }  else {
        authorListBookType.put(e, new ArrayList<>(Collections.singletonList(b.getBookType())));
     }
}));

But it isn't a Stream API based solution and I've gotten in stuck and I don't know how to finish it properly. I know that I must use grouping collector to obtain the required Map<Author, List<BookType>> straight from streams.

Could you give me some hints, please?

like image 396
Pasha Avatar asked Apr 15 '18 00:04

Pasha


2 Answers

You should pair each author of each book with its book type, then collect:

Map<Author, Set<BookType>> authorListBookType = books.stream()
    .flatMap(book -> book.getAuthors().stream()
            .map(author -> Map.entry(author, book.getType())))
    .collect(Collectors.groupingBy(
            Map.Entry::getKey,
            Collectors.mapping(
                    Map.Entry::getValue,
                    Collectors.toSet())));

Here I've used Java 9's Map.entry(key, value) to create the pairs, but you can use new AbstractMap.SimpleEntry<>(key, value) or any other Pair class at your disposal.

This solution uses Collectors.groupingBy and Collectors.mapping to create the desired Map instance.

As @Bohemian points out in the comments, you need to collect to a Set instead of a List to avoid duplicates.


However, I find the stream-based solution a little bit convoluted, because when you pair authors and book types in Map.Entry instances, you then have to use Map.Entry methods in the Collectors.groupingBy part, thus losing the initial semantics of your solution, as well as some readability...

So here's another solution:

Map<Author, Set<BookType>> authorListBookType = new HashMap<>();
books.forEach(book -> 
    book.getAuthors().forEach(author ->
            authorListBookType.computeIfAbsent(author, k -> new HashSet<>())
        .add(book.getType())));

Both solutions assume Author implements hashCode and equals consistently.

like image 57
fps Avatar answered Oct 09 '22 00:10

fps


I'll be looking for a more efficient solution but, in the meantime, here's a working (but inefficient) solution:

books.stream()
     .map(Book::getAuthors)
     .flatMap(List::stream)
     .distinct()
     .collect(Collectors.toMap(Function.identity(), author -> {
         return books.stream().filter(book -> book.getAuthors().contains(author))
                              .map(Book::getBookType).collect(Collectors.toList());
      }));

I definitely prefer the non-stream solution though. One optimization is to change List<Author> to Set<Author> (as I assume the same author wouldn't be listed twice); searching will be improved, but the solution is still slower than your for-loop due to the stream overhead.

Note: This assumes that you've correctly implemented Author#equals and Author#hashCode.

like image 29
Jacob G. Avatar answered Oct 09 '22 00:10

Jacob G.