Monday, March 9, 2015

JDK 8 Streams and Grouping

I wrote about the powerful features of using JDK 8's Streams with Java collections in the post Stream-Powered Collections Functionality in JDK 8. I did not cover use of the groupingBy Collector reduction operation in that post and so address grouping in this post.

The examples in this post will demonstrate how to combine Collection-backed Streams with groupingBy Collectors to reorganize the underlying Collection's data in groups prescribed by a provided classification. These examples are based on the Movie class and Set of Movie classes described in my earlier post Stream-Powered Collections Functionality in JDK 8.

The next code listing demonstrates how a simple statement can be used to group the provided Set of Movies into a Map of movie ratings (key) to movies with that rating (value). The groupingBy Collector provides this Map as a map of key type (the MpaaRating in this case) to a List of the type of objects being grouped (Movie in this case).

/**
 * Demonstrate use of JDK 8 streams and Collectors.groupingBy to
 * group movies by their MPAA ratings.
 */
private static void demonstrateGroupingByRating()
{
   final Map<MpaaRating, List<Movie>> moviesByRating =
      movies.stream().collect(groupingBy(Movie::getMpaaRating));
   out.println("Movies grouped by MPAA Rating: " + moviesByRating);
}

In the example just shown (and in the examples that follow in this post), statically importing java.util.stream.Collectors.groupingBy allows me to NOT need to scope groupingBy calls with the Collectors class name. This simple code snippet groups the movies by their ratings with the returned Map mapping key of movie rating to Lists of movies associated with each rating. Here is an example of the output when the provided Movie set is the same as in my previously referenced post.

Movies grouped by MPAA Rating: {PG13=[Movie: Inception (2010), SCIENCE_FICTION, PG13, 13], R=[Movie: The Shawshank Redemption (1994), DRAMA, R, 1], PG=[Movie: Raiders of the Lost Ark (1981), ACTION, PG, 31, Movie: Back to the Future (1985), SCIENCE_FICTION, PG, 49, Movie: Star Wars: Episode V - The Empire Strikes Back (1980), SCIENCE_FICTION, PG, 12]}

A specific use of the capability just demonstrated is to generate a Map of unique keys to objects in a Collection to the object of that Collection with that key. This might be useful, for example, when needing to look up objects repeatedly and quickly via map but being provided with the objects of interest in a Set or List instead of a Map. Pretending for the moment that movies have unique titles (they only do for my small set), such functionality can be accomplished as shown in the next code listing.

/**
  * Demonstrate use of JDK 8 streams and Collectors.groupingBy to
  * group movies by their title.
  */
private static void demonstrateGroupingByTitle()
{
   final Map<String, List<Movie>> moviesByTitle =
      movies.stream().collect(groupingBy(Movie::getTitle));
   out.println("Movies grouped by title: " + moviesByTitle);
}

Assuming that title is unique for each movie in the original collection, the code above provides a map of movie title to single-element List containing only the movie for which that title is applicable. Any client wanting to quickly look up a movie by its title could call moviesByTitle.get(String).get(0) to get the full Movie object corresponding to that title. The output of doing this with my simple movie set is shown next.

Movies grouped by title: {The Shawshank Redemption=[Movie: The Shawshank Redemption (1994), DRAMA, R, 1], Star Wars: Episode V - The Empire Strikes Back=[Movie: Star Wars: Episode V - The Empire Strikes Back (1980), SCIENCE_FICTION, PG, 12], Back to the Future=[Movie: Back to the Future (1985), SCIENCE_FICTION, PG, 49], Raiders of the Lost Ark=[Movie: Raiders of the Lost Ark (1981), ACTION, PG, 31], Inception=[Movie: Inception (2010), SCIENCE_FICTION, PG13, 13]}

It is possible to group by two different characteristics. This allows for a Collection to be grouped by one characteristic and then have each of those groups sub-grouped by a second characteristic. For example, the following code listing groups movies by rating and then by genre.

/**
 * Demonstrate use of JDK 8 streams and cascaded groupingBy
 * to group movies by ratings and then by genres within ratings.
 */
private static void demonstrateGroupingByRatingAndGenre()
{
   final Map<MpaaRating, Map<Genre, List<Movie>>> moviesByRatingAndGenre =
      movies.stream().collect(groupingBy(Movie::getMpaaRating, groupingBy(Movie::getGenre)));
   out.println("Movies by rating and genre: " + moviesByRatingAndGenre);
}

The code listing just shown first groups the underlying movies by rating and then groups each movie with a particular group of ratings again, but this time by genre. In other words, we get double-level groups of movies by ratings and genre. Output on my simple set of movies is shown next.

Movies by rating and genre: {PG13={SCIENCE_FICTION=[Movie: Inception (2010), SCIENCE_FICTION, PG13, 13]}, R={DRAMA=[Movie: The Shawshank Redemption (1994), DRAMA, R, 1]}, PG={SCIENCE_FICTION=[Movie: Back to the Future (1985), SCIENCE_FICTION, PG, 49, Movie: Star Wars: Episode V - The Empire Strikes Back (1980), SCIENCE_FICTION, PG, 12], ACTION=[Movie: Raiders of the Lost Ark (1981), ACTION, PG, 31]}}

The groupingBy collector makes it easy to group elements of a List or Set into a map with the grouping characteristic as the key and the objects belonging to each group in a List associated with that grouping characteristic key. This allows one all the advantages of a Map, including use of some of the handy methods on Map that have been introduced with JDK 8.

No comments: