【问题标题】:How to rewrite for-each loop into stream?如何将 for-each 循环重写为流?
【发布时间】:2018-04-15 00:19:58
【问题描述】:

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

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

我的目标是使用 Stream API 从List&lt;Book&gt;派生Map&lt;Author, List&lt;BookType&gt;&gt;

为了实现它,首先,我制作了一个 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&lt;Author, List&lt;BookType&gt;&gt;

请给我一些提示好吗?

【问题讨论】:

  • 为什么需要流解决方案?标准循环通常更易于阅读,并且在绝大多数情况下速度更快。
  • @Bohemian 我的观点也是如此。流很酷,并且非常适合某些用例,但是试图将它们硬塞到每个解决方案中是错误的。它们是一种工具,而不是一种要求。
  • 我需要它来练习流
  • 除了练习流之外,查看MapcomputeIfAbsent() ,它将取代if-then

标签: java lambda java-stream


【解决方案1】:

您应该将每本书的每位作者与其书籍类型配对,然后收集:

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&lt;&gt;(key, value) 或任何其他 Pair 类供您使用。

此解决方案使用Collectors.groupingByCollectors.mapping 创建所需的Map 实例。

正如@Bohemian 在 cmets 中指出的那样,您需要收集到 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

【讨论】:

  • 是否有Collectors.mapping() 单参数?
  • 同意,但您可能想要对书型列表进行重复数据删除,因为您会得到很多欺骗(大多数作者都使用一种类型)。真的,结果应该是Map&lt;Author, Set&lt;BookType&gt;&gt;
【解决方案2】:

我会寻找更有效的解决方案,但与此同时,这里有一个可行(但效率低下)的解决方案:

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&lt;Author&gt; 更改为Set&lt;Author&gt;(因为我假设同一作者不会被列出两次);搜索将得到改进,但由于流开销,该解决方案仍然比您的 for 循环慢。

注意:这里假设您已经正确实现了Author#equalsAuthor#hashCode

【讨论】:

  • 你的意思是listbooks吗?
  • @saka1029 我的错,忘了用books替换我的测试代码。
  • 您的答案返回 Map&lt;Author, List&lt;Book&gt;&gt; 而不是 Map&lt;Author, List&lt;BookType&gt;&gt;
  • @saka1029 我一定有阅读障碍,我可以发誓我读过List&lt;Book&gt;。无论如何,我会改变它!编辑:刚刚改了,再次感谢!
【解决方案3】:

这个答案与@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 表示包含mapToEntryvalueMappermerge 函数的类。

  • Main::mapToEntry 将书籍映射到包含作者和书籍类型的 SimpleEntryflatMap 然后将其折叠为 Stream&lt;? extends AbstractMap.SimpleEntry&lt;Author, BookType&gt;&gt;

  • AbstractMap.SimpleEntry::getKey 是一个映射函数来产生 地图键。

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

我可以从中看到的好处是我们将映射逻辑、合并等与流方法隔离开来,这具有更好的可读性和更易于维护,就好像您想在流管道上进一步应用更复杂的逻辑一样,您只需要查看方法并对其进行修改,而根本不用接触流管道。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-09-03
    • 1970-01-01
    • 2014-03-15
    • 2015-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多