简体   繁体   English

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

[英]How to rewrite for-each loop into stream?

I have a data structure named "Book" that consists of the following fields: 我有一个名为“Book”的数据结构,它包含以下字段:

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. 我的目标是使用Stream API从List<Book>派生Map<Author, List<BookType>>

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: 为了实现它,我首先制作了一个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())));
     }
}));

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. 但它不是基于Stream API的解决方案,而且我已陷入困境,我不知道如何正确完成它。 I know that I must use grouping collector to obtain the required Map<Author, List<BookType>> straight from streams. 我知道我必须使用分组收集器直接从流中获取所需的Map<Author, List<BookType>>

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. 这里我使用Java 9的Map.entry(key, value)来创建对,但是您可以使用new AbstractMap.SimpleEntry<>(key, value)或任何其他Pair类。

This solution uses Collectors.groupingBy and Collectors.mapping to create the desired Map instance. 此解决方案使用Collectors.groupingByCollectors.mapping来创建所需的Map实例。

As @Bohemian points out in the comments, you need to collect to a Set instead of a List to avoid duplicates. 正如@Bohemian在评论中指出的那样,您需要收集到Set而不是List以避免重复。


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... 但是,我发现基于流的解决方案有点复杂,因为当你在Map.Entry实例中配对作者和书籍类型时,你必须在Collectors.groupingBy部分中使用Map.Entry方法,从而失去了初始语义。你的解决方案,以及一些可读性......

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. 两种解决方案都假定Author实现hashCode并且一致地equals

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); 一个优化是将List<Author>更改为Set<Author> (因为我假设同一作者不会被列出两次); searching will be improved, but the solution is still slower than your for-loop due to the stream overhead. 搜索将得到改进,但由于流开销,解决方案仍然比for-loop慢。

Note : This assumes that you've correctly implemented Author#equals and Author#hashCode . 注意 :这假定您已正确实现了Author#equalsAuthor#hashCode

This answer is a little bit similar to @Federico's in the sense that the mapping is the same (+1). 这个答案与@ Federico有点相似,因为映射是相同的(+1)。 The motivation for this answer was to try and solve the problem at hand as well as making it as readable as possible. 这个答案的动机是尝试解决手头的问题,并使其尽可能可读。

First, we need to create a function to hide the logic for the mapping: 首先,我们需要创建一个隐藏映射逻辑的函数:

private static Stream<? extends AbstractMap.SimpleEntry<Author, BookType>> mapToEntry(Book book) {
        return book.getAuthors().stream()
                .map(author -> new AbstractMap.SimpleEntry<>(author, book.getBookType()));
}

Second, we need to create a function for the merging logic: 其次,我们需要为合并逻辑创建一个函数:

private static List<BookType> merge(List<BookType> left, List<BookType> right) {
        left.addAll(right);
        return left;
}

Third, we need to create a function for the valueMapper: 第三,我们需要为valueMapper创建一个函数:

private static List<BookType> valueMapper(AbstractMap.SimpleEntry<Author, BookType> entry){
        return new ArrayList<>(Collections.singletonList(entry.getValue()));
}

Now, one can do: 现在,人们可以这样做:

Map<Author, List<BookType>> resultSet =
                books.stream()
                     .flatMap(Main::mapToEntry)
                     .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey,
                            Main::valueMapper,
                                 Main::merge));

Where Main represents the class containing the mapToEntry , valueMapper and merge functions. 其中Main表示包含mapToEntryvalueMappermerge函数的类。

  • Main::mapToEntry maps books to SimpleEntry 's containing the author and the book type which flatMap then collapses it to a Stream<? extends AbstractMap.SimpleEntry<Author, BookType>> Main::mapToEntry将书籍映射到SimpleEntry ,其中包含作者和书籍类型,然后flatMap将其折叠为Stream<? extends AbstractMap.SimpleEntry<Author, BookType>> Stream<? extends AbstractMap.SimpleEntry<Author, BookType>>

  • AbstractMap.SimpleEntry::getKey is a mapping function to produce the map keys. AbstractMap.SimpleEntry::getKey是一个生成映射键的映射函数。

  • Main::valueMapper is a mapping function to produce values of the map. Main::valueMapper是一个映射函数,用于生成映射的值。
  • Main::merge is a merge function, used to resolve collisions between values associated with the same key. Main::merge是一个合并函数,用于解决与同一个键关联的值之间的冲突。

The benefit I can see from this is that we isolate the mapping logic, merging etc. away from the stream methods, this comes with better readability and easier to maintain as if you want to further apply more complex logic on the stream pipeline, you only have to look at the methods and modify those and not touch the stream pipeline at all. 我可以从中看到的好处是我们隔离了映射逻辑,从流方法中合并等等,这带来了更好的可读性和更易于维护,就好像你想在流管道上进一步应用更复杂的逻辑一样,你只需要必须查看方法并修改它们,而不是触及流管道。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM