Monday, February 2, 2009

The Java SE 6 NavigableMap

I have written and blogged previously about some of my favorite Java SE 6 features such as the Deque, [Sun's] inclusion of VisualVM, [Sun's] Java HTTP Server, custom JMX MXBeans, String.isEmpty(), and [Sun's] inclusion of JAXB and annotations processing. In this blog posting, I intend to discuss the NavigableMap, a Java Collections interface that I don't use often, but which comes in very handy in certain situations.

The NavigableMap extends the SortedMap and adds methods to this interface specifically designed for easy navigation (hence the name). The next code listing, for the FavoriteMovies class, demonstrates how easy it is to apply the NavigableMap.

FavoriteMovies.java


package dustin.examples.navigable;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;

/**
* Demonstrate a NavigableMap.
*
* @author Dustin
*/
public class FavoriteMovies
{
/** New line separator. */
private static final String NEW_LINE = System.getProperty("line.separator");

/** Header bar used for separation of output sections. */
private static final String HEADER_BAR =
"=======================================================================";

/**
* Favorite movies with Integer key represent the movie's rating in the
* favorites and the value being the movie itself.
*/
private NavigableMap<Integer, Movie> favoriteMovies =
new TreeMap<Integer, Movie>();

/**
* Add a movie to my favorites.
*
* @param newRanking Ranking of movie being added to favorites.
* @param newMovie Movie being added to favorites.
*/
public void addMovie(
final Integer newRanking, final Movie newMovie)
{
favoriteMovies.put(newRanking, newMovie);
}

/**
* Write a header with the provided headerText to the provided OutputStream.
*
* @param headerText Text to be written in header.
* @param out OutputStream to which to write header.
* @throws java.io.IOException Thrown if header cannot be written to the
* provided OutputStream.
*/
private void printHeader(
final String headerText, final OutputStream out) throws IOException
{
out.write(NEW_LINE.getBytes());
out.write(HEADER_BAR.getBytes());
out.write(NEW_LINE.getBytes());
out.write("= ".getBytes());
out.write(headerText.getBytes());
out.write(NEW_LINE.getBytes());
out.write(HEADER_BAR.getBytes());
out.write(NEW_LINE.getBytes());
}

/**
* Print the contents of my NavigableMap to the provided OutputStream.
*
* @throws IOException Thrown if my contents cannot be written to the
* provided OutputStream.
*/
public void printContents(final OutputStream out) throws IOException
{
printHeader("Contents of Navigable Map", out);
for (Entry<Integer,Movie> favoriteMovie : this.favoriteMovies.entrySet())
{
out.write("Movie Rank #".getBytes());
out.write(String.valueOf(favoriteMovie.getKey()).getBytes());
out.write(": ".getBytes());
out.write(favoriteMovie.getValue().getTitle().getBytes());
out.write(NEW_LINE.getBytes());
}
}

/**
* Write provided label and provided text to provided OutputStream.
*
* @param label Label to be written to OutputStream.
* @param navigableMapText Text to be written to OutputStream.
* @param out OutputStream to which to write label and text.
* @throws java.io.IOException Thrown if exception occurs writing to the
* provided OutputStream.
*/
private void printNavigableMapApproach(
final String label,
final String navigableMapText,
final OutputStream out) throws IOException
{
out.write(label.getBytes());
out.write(": ".getBytes());
out.write(navigableMapText.getBytes());
out.write((NEW_LINE+NEW_LINE).getBytes());
}

/**
* Write the provided Navigable Map to the provided OutputStream with the
* provided label.
*
* @param label Label to be written to OutputStream.
* @param moviesNavigableMap Movies NavigableMap to be written to OutputStream.
* @param out OutputStream to which NavigableMap and its label are to be written.
* @throws java.io.IOException Thrown if exception occurs when writing to the
* provided OutputStream.
*/
private void printMoviesNavigableMap(
final String label,
final NavigableMap<Integer, Movie> moviesNavigableMap,
final OutputStream out) throws IOException
{
out.write(label.getBytes());
out.write(": ".getBytes());
out.write(NEW_LINE.getBytes());
for ( NavigableMap.Entry<Integer,Movie> entry :
moviesNavigableMap.entrySet())
{
out.write("\t#".getBytes());
out.write(entry.getKey().toString().getBytes());
out.write(": ".getBytes());
out.write(entry.getValue().getTitle().getBytes());
out.write(NEW_LINE.getBytes());
}
out.write(NEW_LINE.getBytes());
}

/**
* Write demonstration of several NavigableMap methods to provided
* OutputStream.
*
* @param out OutputStream to which to write output of examples using
* NavigableMap.
* @throws java.io.IOException Thrown if output cannot be written to the
* provided OutputStream.
*/
public void demonstrateNavigableMap(final OutputStream out) throws IOException
{
printHeader("Select NavigableMap Methods", out);

// Demonstrate NavigableMap.firstEntry()
printNavigableMapApproach(
"First Movie [firstEntry()]",
favoriteMovies.firstEntry().getValue().getTitle(),
out);

// Demonstrate NavigableMap.lastEntry()
printNavigableMapApproach(
"Last Movie [lastEntry()]",
favoriteMovies.lastEntry().getValue().getTitle(),
out);

// Demonstrate NavigableMap.floorEntry
printNavigableMapApproach(
"Floor Entry for '6' [floorEntry(K)]",
favoriteMovies.floorEntry(6).getValue().getTitle(),
out);

// Demonstrate NavigableMap.ceilingEntry
printNavigableMapApproach(
"Ceiling Entry for '3' [ceilingEntry(K)]",
favoriteMovies.ceilingEntry(3).getValue().getTitle(),
out);

// Demonstrate NavigableMap.headMap:
// First (lowest) entry automatically assumed; 3 is inclusive (true)
final NavigableMap<Integer, Movie> topMovies =
this.favoriteMovies.headMap(3, true);
printMoviesNavigableMap(
"Top range of movies '1' through '3' [inclusive] - headMap(K,Boolean)",
topMovies,
out);

// Demonstrate NavigableMap.subMap:
// Make lower (from) number inclusive (true) and higher (to) number
// exclusive (false).
final NavigableMap<Integer, Movie> middleMovies =
this.favoriteMovies.subMap(4, true, 7, false);
printMoviesNavigableMap(
"Middle range of movies '4' through '6' [7 not inclusive] - subMap"
+ "(K,Boolean,K,Boolean)",
middleMovies,
out);

// Demonstrate NavigableMap.tailMap:
// Last (highest) entry automatically assumed; 6 is exclusive (false)
final NavigableMap<Integer, Movie> bottomOfBestMovies =
this.favoriteMovies.tailMap(6, false);
printMoviesNavigableMap(
"Bottom range of best movies '7' through '10' [6 not inclusive] "
+ "- tailMap(K,Boolean)",
bottomOfBestMovies,
out);
}

/**
* Set up an instance of me with pre-populated data.
*
* @return Instance of me with pre-populated data (for testing and
* demonstration of this class).
*/
public static FavoriteMovies setUpFavoriteMovies()
{
final FavoriteMovies movies = new FavoriteMovies();
movies.addMovie(
3,
new Movie.Builder().title("Raiders of the Lost Ark")
.genre(MovieGenre.FANTASY)
.build());
movies.addMovie(
2,
new Movie.Builder().title("Star Wars: The Empire Strikes Back")
.genre(MovieGenre.SCIENCE_FICTION)
.build());
movies.addMovie(
4,
new Movie.Builder().title("Men in Black")
.genre(MovieGenre.SCIENCE_FICTION)
.build());
movies.addMovie(
1,
new Movie.Builder().title("Fletch")
.genre(MovieGenre.COMEDY)
.build());
movies.addMovie(
5,
new Movie.Builder().title("Ocean's Eleven")
.genre(MovieGenre.ACTION)
.build());
movies.addMovie(
9,
new Movie.Builder().title("The Outlaw Josey Wales")
.genre(MovieGenre.WESTERN)
.build());
movies.addMovie(
8,
new Movie.Builder().title("Groundhog Day")
.genre(MovieGenre.COMEDY)
.build());
movies.addMovie(
10,
new Movie.Builder().title("The Sixth Sense")
.genre(MovieGenre.HORROR)
.build());
movies.addMovie(
7,
new Movie.Builder().title("War Games")
.genre(MovieGenre.SCIENCE_FICTION)
.build());
movies.addMovie(
6,
new Movie.Builder().title("The Princess Bride")
.genre(MovieGenre.COMEDY)
.build());
return movies;
}

/**
* Main executable to demonstrate NavigableMap.
*
* @param arguments Command-line arguments; none anticipated.
*/
public static void main(final String[] arguments)
{
final FavoriteMovies movies = setUpFavoriteMovies();
try
{
movies.printContents(System.out);
movies.demonstrateNavigableMap(System.out);
}
catch (Exception ex)
{
System.err.println("Exception encountered: " + ex.toString());
}
}
}



The above code populates a TreeMap implementation of NavigableMap with information about some of my favorite movies and then invokes several methods on that interface to demonstrate the NavigableMap.

When the above class is run, the output appears as shown next:



The output screen snapshot shows how several significant NavigableMap methods work. It also demonstrates the automatic sorting supported by the NavigableMap as a specialized SortedMap: the values were put into the TreeMap intentionally out of order, but are printed in order when the NavigableMap is traversed using the for-each loop.

For completeness, I include the simple code listings for the Movie class and for the MovieGenre class.

Movie.java


package dustin.examples.navigable;

/**
* Class representating a movie.
*
* @author Dustin
*/
public class Movie
{
/** Title of the movie. */
private String title;

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

/** Movie's primary director. */
private String director;

/** Movie's primary genre. */
private MovieGenre genre;

/** No-arguments constructor not intended for public consumption. */
private Movie() {}

/**
* Provide my director.
*
* @return My director.
*/
public String getDirector()
{
return this.director;
}

/**
* Provide my genre.
*
* @return My genre.
*/
public MovieGenre getGenre()
{
return this.genre;
}

/**
* Provide my title.
*
* @return My title.
*/
public String getTitle()
{
return this.title;
}

/**
* Provide my rear of release.
*
* @return The year of my release.
*/
public int getYear()
{
return this.year;
}

/**
* Provide a String representation of me.
*
* @return My String representation.
*/
@Override
public String toString()
{
final StringBuilder builder = new StringBuilder();
builder.append("Title: ").append(this.title);
builder.append("; Year: ").append(this.year);
builder.append("; Director: ").append(this.director);
builder.append("; Genre: ").append(this.genre);
return builder.toString();
}

public static class Builder
{
private String title;
private int year;
private String director;
private MovieGenre genre;

public Builder() {}

public Builder director(final String newDirector)
{
this.director = newDirector;
return this;
}

public Builder genre(final MovieGenre newGenre)
{
this.genre = newGenre;
return this;
}

public Builder title(final String newTitle)
{
this.title = newTitle;
return this;
}

public Builder year(int newYear)
{
this.year = newYear;
return this;
}

public Movie build()
{
return new Movie(this);
}
}

/**
* Constructor intended to be used in conjunction with Builder.
*
* @param builder Builder for building an instance of me.
*/
private Movie(final Builder builder)
{
this.title = builder.title;
this.director = builder.director;
this.year = builder.year;
this.genre = builder.genre;
}
}



MovieGenre.java


package dustin.examples.navigable;

/**
* Represent a movie genre.
*
* @author Dustin
*/
public enum MovieGenre
{
ACTION,
COMEDY,
DRAMA,
FANTASY,
HORROR,
ROMANTIC_COMEDY,
SCIENCE_FICTION,
WESTERN
}



Conclusion

The Java SE 6 NavigableMap is not something I use on a daily basis, but it has come in handy now and then. It is easy to apply and, in the appropriate situations, makes it really easy to navigate a Map. Although not covered here, there is also a NavigableSet that does for the Set what NavigableMap does for the Map. In fact, the NavigableMap.navigableKeySet() method returns a NavigableSet.

In honor of today being Groundhog Day, it is time to watch the eighth movie on today's list: Groundhog Day.

1 comment:

GKB said...

Groundhog Day is one of my favorite movies. Your list is solid, but "The Outlaw Josey Whales"? I've never seen it, but I have to disagree with your choice anyway. Let me offer a few alternatives so I can hopefully get your mind right.

Big
Blade Runner
E.T.
Toy Story
Jaws

I eagerly await your revised list.