我有一个名为" Book"的数据结构。它包含以下字段:
public final class Book {
private final String title;
private final BookType bookType;
private final List<Author> authors;
}
我的目标是使用Stream API从Map<Author, List<BookType>>
派生List<Book>
。
为了实现这一目标,我首先做了一个for-each循环来阐明解决方案的步骤,并在我逐步将其重写为基于流的方法之后:
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())));
}
}));
但它不是基于Stream API的解决方案,而且我已陷入困境,而且我不知道如何正确完成它。
我知道我必须使用分组收集器直接从流中获取所需的Map<Author, List<BookType>>
。
请你给我一些提示吗?
答案 0 :(得分:8)
您应该将每本书的每位作者与其书籍类型配对,然后收集:
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())));
我已经使用Java 9 Map.entry(key, value)
创建了对,但您可以使用new AbstractMap.SimpleEntry<>(key, value)
或任何其他Pair
课程。
此解决方案使用Collectors.groupingBy
和Collectors.mapping
创建所需的Map
实例。
正如@Bohemian在评论中指出的那样,您需要收集到Set
而不是List
以避免重复。
但是,我发现基于流的解决方案有点复杂,因为当您在Map.Entry
个实例中配对作者和书籍类型时,您必须在Map.Entry
中使用Collectors.groupingBy
方法部分,因此失去了解决方案的初始语义,以及一些可读性......
所以这是另一个解决方案:
Map<Author, Set<BookType>> authorListBookType = new HashMap<>();
books.forEach(book ->
book.getAuthors().forEach(author ->
authorListBookType.computeIfAbsent(author, k -> new HashSet<>())
.add(book.getType())));
两种解决方案都假设Author
始终如一地实现hashCode
和equals
。
答案 1 :(得分:3)
我会寻找更有效的解决方案,但与此同时,这是一个有效(但效率低)的解决方案:
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());
}));
我绝对更喜欢非流解决方案。一个优化是将List<Author>
更改为Set<Author>
(因为我假设同一作者不会被列出两次);搜索将得到改进,但由于流开销,解决方案仍然比你的for循环慢。
注意:这假设您已正确实施Author#equals
和Author#hashCode
。
答案 2 :(得分:1)
这个答案与@ Federico有点类似,因为映射是相同的(+1)。这个答案的动机是尝试解决手头的问题,并使其尽可能可读。
首先,我们需要创建一个隐藏映射逻辑的函数:
private static Stream<? extends AbstractMap.SimpleEntry<Author, BookType>> mapToEntry(Book book) {
return book.getAuthors().stream()
.map(author -> new AbstractMap.SimpleEntry<>(author, book.getBookType()));
}
其次,我们需要为合并逻辑创建一个函数:
private static List<BookType> merge(List<BookType> left, List<BookType> right) {
left.addAll(right);
return left;
}
第三,我们需要为valueMapper创建一个函数:
private static List<BookType> valueMapper(AbstractMap.SimpleEntry<Author, BookType> entry){
return new ArrayList<>(Collections.singletonList(entry.getValue()));
}
现在,人们可以这样做:
Map<Author, List<BookType>> resultSet =
books.stream()
.flatMap(Main::mapToEntry)
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey,
Main::valueMapper,
Main::merge));
Main
代表包含mapToEntry
,valueMapper
和merge
函数的类。
Main::mapToEntry
将图书映射到SimpleEntry
,其中包含作者和图书类型flatMap
然后将其折叠为Stream<? extends AbstractMap.SimpleEntry<Author, BookType>>
AbstractMap.SimpleEntry::getKey
是要生成的映射函数
地图键。
Main::valueMapper
是一个用于生成值的映射函数
地图。Main::merge
是一个合并函数,用于解决之间的冲突
与相同密钥关联的值。我可以从中看到的好处是我们隔离了映射逻辑,从流方法中合并等等,这带来了更好的可读性和更易于维护,就好像你想在流管道上进一步应用更复杂的逻辑一样,您只需要查看方法并修改它们,而不是触及流管道。