如何将每个循环重写为流?

时间:2018-04-15 00:19:59

标签: java lambda java-stream

我有一个名为" 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>>

请你给我一些提示吗?

3 个答案:

答案 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.groupingByCollectors.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始终如一地实现hashCodeequals

答案 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#equalsAuthor#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代表包含mapToEntryvalueMappermerge函数的类。

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

  • AbstractMap.SimpleEntry::getKey是要生成的映射函数 地图键。

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

我可以从中看到的好处是我们隔离了映射逻辑,从流方法中合并等等,这带来了更好的可读性和更易于维护,就好像你想在流管道上进一步应用更复杂的逻辑一样,您只需要查看方法并修改它们,而不是触及流管道。