Home > front end >  Java Stream API - Group by items of an object's inner list
Java Stream API - Group by items of an object's inner list

Time:11-01

Is there a way to achieve the following example code, leveraging Java Stream API rather than having to create a HashMap and populate it inside double forEaches? I was trying to play around with groupingBy and flatMap but couldn't find a way out.

Having a list of Movies, where each one has a list of genres (Strings)...

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

...I want to group all the movies by genre

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

CodePudding user response:

This one is tricky because you cannot define a key for each Movie since such an object can appear under multiple keys.

The best solution is as far as I know equal to yours:

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

If you want to "convert" this snippet into , you have to start with what you have - which is the individual genres. Extract them from each Movie using flatMap and distinct to avoid duplicates. Then use a Collector.toMap to get the desired output.

  • Key: Function.identity() to map each unique genre as a key itself.
  • Value: Use another Stream to filter out the movies containing a particular genre to assign them to the key.
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())));

The procedural approach in the first snippet is faster, easier to read and understand. I don't recommend using here.


A note.. there is no meaning of using forEach right after stream: The sequence of list.stream().forEach(...) can be list.forEach(...) instead.

CodePudding user response:

First we might want to create a Pair or Tuple as follows:

    public static class Pair {
        Movie movie;
        String genre;

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

    }

This can be a Record as well in Java 17.

Next, we can do the following with 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;
                        }));

Or we can use 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())));

Essentially we create a Pair for every movie with its every possible genre. After we have this pairs, we are grouping them in a way to get rid of the Pair.

  • Related