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 forEach
es? 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 java-stream, 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 uniquegenre
as a key itself. - Value: Use another
Stream
to filter out the movies containing a particulargenre
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 java-stream 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
.