How to rewrite for-each loop into stream?

Pasha :

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?

Federico Peralta Schaffner :

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.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=440517&siteId=1