Tuesday, February 17, 2015

Using JDK 8 Streams to Convert Between Collections of Wrapped Objects and Collections of Wrapper Objects

I have found Decorators and Adapters to be useful from time to time as I have worked with Java-based applications. These "wrappers" work well in a variety of situations and are fairly easy to understand and implement, but things can become a bit more tricky when a hierarchy of objects rather than a single object needs to be wrapped. In this blog post, I look at how Java 8 streams make it easier to convert between collections of objects and collections of objects that wrap those objects.

For this discussion, I'll apply two simple Java classes representing a Movie class and a class that "wraps" that class called MovieWrapper. The Movie class was used in my post on JDK 8 enhancements to Java collections. The Movie class and the class that wraps it are shown next.

Movie.java
package dustin.examples.jdk8.streams;

import java.util.Objects;

/**
 * Basic characteristics of a motion picture.
 *
 * @author Dustin
 */
public class Movie
{
   /** Title of movie. */
   private final String title;

   /** Year of movie's release. */
   private final int yearReleased;

   /** Movie genre. */
   private final Genre genre;

   /** MPAA Rating. */
   private final MpaaRating mpaaRating;

   /** imdb.com Rating. */
   private final int imdbTopRating;

   public Movie(final String newTitle, final int newYearReleased,
                final Genre newGenre, final MpaaRating newMpaaRating,
                final int newImdbTopRating)
   {
      this.title = newTitle;
      this.yearReleased = newYearReleased;
      this.genre = newGenre;
      this.mpaaRating = newMpaaRating;
      this.imdbTopRating = newImdbTopRating;
   }

   public String getTitle()
   {
      return this.title;
   }

   public int getYearReleased()
   {
      return this.yearReleased;
   }

   public Genre getGenre()
   {
      return this.genre;
   }

   public MpaaRating getMpaaRating()
   {
      return this.mpaaRating;
   }

   public int getImdbTopRating()
   {
      return this.imdbTopRating;
   }

   @Override
   public boolean equals(Object other)
   {
      if (!(other instanceof Movie))
      {
         return false;
      }
      final Movie otherMovie = (Movie) other;
      return   Objects.equals(this.title, otherMovie.title)
            && Objects.equals(this.yearReleased, otherMovie.yearReleased)
            && Objects.equals(this.genre, otherMovie.genre)
            && Objects.equals(this.mpaaRating, otherMovie.mpaaRating)
            && Objects.equals(this.imdbTopRating, otherMovie.imdbTopRating);
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(this.title, this.yearReleased, this.genre, this.mpaaRating, this.imdbTopRating);
   }

   @Override
   public String toString()
   {
      return "Movie: " + this.title + " (" + this.yearReleased + "), " + this.genre + ", " + this.mpaaRating + ", "
            + this.imdbTopRating;
   }
}
MovieWrapper.java
package dustin.examples.jdk8.streams;

/**
 * Wraps a movie like a Decorator or Adapter might.
 * 
 * @author Dustin
 */
public class MovieWrapper
{
   private Movie wrappedMovie;

   public MovieWrapper(final Movie newMovie)
   {
      this.wrappedMovie = newMovie;
   }

   public Movie getWrappedMovie()
   {
      return this.wrappedMovie;
   }

   public void setWrappedMovie(final Movie newMovie)
   {
      this.wrappedMovie = newMovie;
   }

   public String getTitle()
   {
      return this.wrappedMovie.getTitle();
   }

   public int getYearReleased()
   {
      return this.wrappedMovie.getYearReleased();
   }

   public Genre getGenre()
   {
      return this.wrappedMovie.getGenre();
   }

   public MpaaRating getMpaaRating()
   {
      return this.wrappedMovie.getMpaaRating();
   }

   public int getImdbTopRating()
   {
      return this.wrappedMovie.getImdbTopRating();
   }

   @Override
   public String toString()
   {
      return this.wrappedMovie.toString();
   }
}

With the Movie and MovieWrapper classes defined above, I now look at converting a collection of one of these into a collection of the other. Before JDK 8, a typical approach to convert a collection of Movie objects into a collection of MovieWrapper objects would to iterate over the source collection of Movie objects and add each one to a new collection of MovieWrapper objects. This is demonstrated in the next code listing.

Converting Collection of Wrapped Object Into Collection of Wrapper Objects
// movies previously defined as Set<Movie>
final Set<MovieWrapper> wrappedMovies1 = new HashSet<>();
for (final Movie movie : movies)
{
   wrappedMovies1.add(new MovieWrapper(movie));
}

With JDK 8 streams, the operation above can now be implemented as shown in the next code listing.

Converting Collection of Wrapped Objects Into Collection of Wrapper Objects - JDK 8
// movies previously defined as Set<Movie>
final Set<MovieWrapper> wrappedMovies2 =
   movies.stream().map(movie -> new MovieWrapper(movie)).collect(Collectors.toSet());

Converting the other direction (from collection of wrapper objects to collection of wrapped objects) can be similarly compared to demonstrate how JDK 8 changes this. The next two code listings show the old way and the JDK 8 way.

Converting Collection of Wrapper Objects Into Collection of Wrapped Objects
final Set<Movie> newMovies1 = new HashSet();
for (final MovieWrapper wrappedMovie : wrappedMovies1)
{
   newMovies1.add(wrappedMovie.getWrappedMovie());
}
Converting Collection of Wrapper Objects Into Collection of Wrapped Objects - JDK 8
final Set<Movie> newMovies2 =
   wrappedMovies2.stream().map(MovieWrapper::getWrappedMovie).collect(Collectors.toSet());

Like some of the examples in my post Stream-Powered Collections Functionality in JDK 8, the examples in this post demonstrate the power of aggregate operations provided in JDK 8. The advantages of these aggregate operations over traditional iteration include greater conciseness in the code, arguably (perhaps eventually) greater readability, and the advantages of internal iteration (including easier potential streams-supported parallelization). A good example of using streams and more complex Functions to convert between collections of less cohesively related objects is shown in Transform object into another type with Java 8.

No comments: