2

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)));

3 Answers 3

4

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.

Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

0
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.