如何将for-each循环改写为流?

9

我有一个名为“Book”的数据结构,其中包含以下字段:

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

我的目标是使用Stream API从List中推导出Map>。为了实现这个目标,首先我使用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())));
     }
}));

但这不是基于流API的解决方案,我遇到了困难,不知道如何正确地完成它。我知道我必须使用分组收集器从流中直接获得所需的Map>。 请给我一些提示,谢谢!

3
为什么需要流式解决方案?标准循环通常更易于阅读,在绝大多数情况下也更快。 - Bohemian
@Bohemian 我也有同感。流处理很酷,对某些用例非常有用。但是,试图把它们塞入每个解决方案中都是错误的。它们只是一种工具,并不是必要条件。 - xtratic
2
我需要它来练习流处理。 - Pasha
1
除了练习流之外,还可以查看MapcomputeIfAbsent()来替换if-then - daniu
3个回答

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.groupingByCollectors.mapping来创建所需的Map实例。

正如@Bohemian在评论中指出的那样,你需要收集到一个Set而不是List,以避免重复项。


然而,我发现基于流的解决方案有点混乱,因为当你在Map.Entry实例中配对作者和书籍类型时,你随后必须在Collectors.groupingBy部分使用Map.Entry方法,从而失去了你的解决方案的初始语义以及一些可读性...

所以这里提供另一种解决方案:

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

两种解决方案都假设Author类一致地实现了hashCodeequals方法。


1
有没有带单个参数的Collectors.mapping()函数? - user4910279
同意,但你可能想要去重书籍类型列表,因为你会得到很多重复项(大多数作者只写一种类型的书)。实际上,结果应该是 Map<Author, Set<BookType>> - Bohemian

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#equalsAuthor#hashCode


你的意思是list就是books吗? - user4910279
@saka1029 抱歉,我忘了用books替换我的测试代码。 - Jacob G.
你的答案返回了 Map<Author, List<Book>> 而不是 Map<Author, List<BookType>> - user4910279
@saka1029,我可能是有阅读障碍,我本来以为我读到的是List<Book>。不管怎样,我会改正的!编辑:已经修改了,再次感谢! - Jacob G.

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

第三步,我们需要创建一个用于值映射的函数:
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表示包含mapToEntryvalueMappermerge函数的类。

  • Main::mapToEntry将书籍映射到包含作者和书籍类型的SimpleEntry,然后flatMap将其折叠为一个Stream<? extends AbstractMap.SimpleEntry<Author, BookType>>

  • AbstractMap.SimpleEntry::getKey是用于生成映射键的映射函数。

  • Main::valueMapper是用于生成映射值的映射函数。
  • Main::merge是一个合并函数,用于解决与同一键相关联的值之间的冲突。

我能看出来的好处是我们将映射逻辑、合并等与流方法隔离开来,这样可以更好地阅读并更容易维护,因为如果要在流管道上进一步应用更复杂的逻辑,您只需要查看方法并修改它们,而不必触及流管道。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接