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?
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.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With