Java 流 API - 按对象内部列表的项目分组

Java Stream API - Group by items of an object's inner list

有没有办法实现以下示例代码,利用 Java 流 API 而不是必须创建 HashMap 并将其填充到双 forEaches 中?我试着玩 groupingByflatMap 但找不到出路。

有一个电影列表,其中每个电影都有一个流派列表(字符串)...

class Movie {
    List<String> genres;
}
List<Movie> movies = new ArrayList<>();

...我想按流派对所有电影进行分组

Map<String, List<Movie>> moviesByGenre = new HashMap();
movies.stream()
        .forEach(movie -> movie.getGenres()
                .stream()
                .forEach(genre -> moviesByGenre
                        .computeIfAbsent(genre, k -> new ArrayList<>())
                        .add(movie)));

这个很棘手,因为您不能为每个键定义一个键 Movie,因为这样的对象可以出现在多个键下。

据我所知,最佳 解决方案与您的相同:

Map<String, List<Movie>> groupedMovies = new HashMap<>();
movies.forEach(movie -> {
    movie.getGenres().forEach(genre ->
        groupedMovies.computeIfAbsent(genre, g -> new ArrayList<>()).add(movie)
    );
});

如果您想将此片段“转换”为 ,您必须从 您拥有的 开始 - 即各个流派。使用 flatMapdistinct 从每个 Movie 中提取它们以避免重复。然后使用 Collector.toMap 获得所需的输出。

  • Key: Function.identity() 将每个唯一的 genre 映射为键本身。
  • 值: 使用另一个 Stream 过滤掉包含特定 genre 的电影以将它们分配给键。
Map<String, List<Movie>> groupedMovies = movies.stream()
    .map(Movie::getGenres)
    .flatMap(List::stream)
    .distinct()
    .collect(Collectors.toMap(
            Function.identity(),
            genre -> movies.stream()
                           .filter(movie -> movie.getGenres().contains(genre))
                           .collect(Collectors.toList())));

第一个片段中的过程方法更快,更易于阅读和理解。我不建议在这里使用


注意.. 在 stream 之后使用 forEach 没有任何意义:list.stream().forEach(...) 的序列可以改为 list.forEach(...)

首先,我们可能想创建一个 PairTuple,如下所示:

    public static class Pair {
        Movie movie;
        String genre;

        public Pair(Movie movie, String genre) {
            this.movie = movie;
            this.genre = genre;
        }
       
       // Getters omitted

    }

这也可以是 Record 以及 Java 17.

接下来,我们可以用toMap进行以下操作:

Map<String, List<Movie>> map = movies.stream()
                .flatMap(movie -> movie.getGenres().stream().map(genre -> new Pair(movie, genre)))
                .collect(Collectors.toMap(Pair::getGenre, pair -> new ArrayList<>(List.of(pair.getMovie())),
                        (existing, current) -> {
                            existing.addAll(current);
                            return existing;
                        }));

或者我们可以使用groupBy:

Map<String, List<Movie>> map = movies.stream()
                .flatMap(movie -> movie.getGenres().stream().map(genre -> new Pair(movie, genre)))
                .collect(Collectors.groupingBy(Pair::getGenre,
                        HashMap::new,
                        Collectors.mapping(Pair::getMovie, Collectors.toList())));

本质上,我们为每部电影创建了一个 Pair,其中包含所有可能的 genre。在我们有了这对之后,我们将它们分组以摆脱 Pair.

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.groupingBy;


class Movie {
        String type;
        String name;
        Movie(String type, String name)
        {
            this.type = type;
            this.name = name;
        }

    public Movie(String type) {
            this.type = type;
    }

    public String getType() {
        return type;
    }
    public String getName()
    {
        return name;
    }

    public static void main(String... args) {
        List<Movie> posts = new ArrayList<>();
        posts.add( new Movie("HORROR", "movie1"));
        posts.add( new Movie("HORROR", "movie1"));
        posts.add( new Movie("HORROR", "movie1"));
        posts.add( new Movie("COMEDY", "movie1"));
        posts.add( new Movie("COMEDY", "movie2"));
        posts.add( new Movie("COMEDY", "movie3"));
        posts.add( new Movie("COMEDY", "movie2"));


        Map<String, List<Movie>>postsPerType = posts.stream()
            .collect(groupingBy(Movie::getType));

        postsPerType.forEach((k,v) -> System.out.println("Key = "
            + k + ", Value = " + v.size()));

        Map<String, List<Movie>>postsPerName = posts.stream()
            .collect(groupingBy(Movie::getName));
        postsPerName.forEach((k,v) -> System.out.println("Key = "
            + k + ", Value = " + v.size()));

    }
}

Key = HORROR, Value = 3
Key = COMEDY, Value = 4
Key = movie3, Value = 1
Key = movie2, Value = 2
Key = movie1, Value = 4