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

  1. package dustin.examples.navigable;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.OutputStream;  
  5. import java.util.Map.Entry;  
  6. import java.util.NavigableMap;  
  7. import java.util.TreeMap;  
  8.   
  9. /** 
  10.  * Demonstrate a NavigableMap. 
  11.  *  
  12.  * @author Dustin 
  13.  */  
  14. public class FavoriteMovies  
  15. {  
  16.    /** New line separator. */  
  17.    private static final String NEW_LINE = System.getProperty("line.separator");  
  18.   
  19.    /** Header bar used for separation of output sections. */  
  20.    private static final String HEADER_BAR =  
  21.       "=======================================================================";  
  22.   
  23.    /** 
  24.     * Favorite movies with Integer key represent the movie's rating in the 
  25.     * favorites and the value being the movie itself. 
  26.     */  
  27.    private NavigableMap<Integer, Movie> favoriteMovies =  
  28.       new TreeMap<Integer, Movie>();  
  29.   
  30.    /** 
  31.     * Add a movie to my favorites. 
  32.     *  
  33.     * @param newRanking Ranking of movie being added to favorites. 
  34.     * @param newMovie Movie being added to favorites. 
  35.     */  
  36.    public void addMovie(  
  37.       final Integer newRanking, final Movie newMovie)  
  38.    {  
  39.       favoriteMovies.put(newRanking, newMovie);  
  40.    }  
  41.   
  42.    /** 
  43.     * Write a header with the provided headerText to the provided OutputStream. 
  44.     *  
  45.     * @param headerText Text to be written in header. 
  46.     * @param out OutputStream to which to write header. 
  47.     * @throws java.io.IOException Thrown if header cannot be written to the 
  48.     *    provided OutputStream. 
  49.     */  
  50.    private void printHeader(  
  51.       final String headerText, final OutputStream out) throws IOException  
  52.    {  
  53.       out.write(NEW_LINE.getBytes());  
  54.       out.write(HEADER_BAR.getBytes());  
  55.       out.write(NEW_LINE.getBytes());  
  56.       out.write("=  ".getBytes());  
  57.       out.write(headerText.getBytes());  
  58.       out.write(NEW_LINE.getBytes());  
  59.       out.write(HEADER_BAR.getBytes());  
  60.       out.write(NEW_LINE.getBytes());  
  61.    }  
  62.   
  63.    /** 
  64.     * Print the contents of my NavigableMap to the provided OutputStream. 
  65.     *  
  66.     * @throws IOException Thrown if my contents cannot be written to the 
  67.     *    provided OutputStream. 
  68.     */  
  69.    public void printContents(final OutputStream out) throws IOException  
  70.    {  
  71.       printHeader("Contents of Navigable Map", out);  
  72.       for (Entry<Integer,Movie> favoriteMovie : this.favoriteMovies.entrySet())  
  73.       {  
  74.          out.write("Movie Rank #".getBytes());  
  75.          out.write(String.valueOf(favoriteMovie.getKey()).getBytes());  
  76.          out.write(": ".getBytes());  
  77.          out.write(favoriteMovie.getValue().getTitle().getBytes());  
  78.          out.write(NEW_LINE.getBytes());  
  79.       }  
  80.    }  
  81.   
  82.    /** 
  83.     * Write provided label and provided text to provided OutputStream. 
  84.     *  
  85.     * @param label Label to be written to OutputStream. 
  86.     * @param navigableMapText Text to be written to OutputStream. 
  87.     * @param out OutputStream to which to write label and text. 
  88.     * @throws java.io.IOException Thrown if exception occurs writing to the 
  89.     *    provided OutputStream. 
  90.     */  
  91.    private void printNavigableMapApproach(  
  92.       final String label,  
  93.       final String navigableMapText,  
  94.       final OutputStream out) throws IOException  
  95.    {  
  96.       out.write(label.getBytes());  
  97.       out.write(": ".getBytes());  
  98.       out.write(navigableMapText.getBytes());  
  99.       out.write((NEW_LINE+NEW_LINE).getBytes());  
  100.    }  
  101.   
  102.    /** 
  103.     * Write the provided Navigable Map to the provided OutputStream with the 
  104.     * provided label. 
  105.     *  
  106.     * @param label Label to be written to OutputStream. 
  107.     * @param moviesNavigableMap Movies NavigableMap to be written to OutputStream. 
  108.     * @param out OutputStream to which NavigableMap and its label are to be written. 
  109.     * @throws java.io.IOException Thrown if exception occurs when writing to the 
  110.     *    provided OutputStream. 
  111.     */  
  112.    private void printMoviesNavigableMap(  
  113.       final String label,  
  114.       final NavigableMap<Integer, Movie> moviesNavigableMap,  
  115.       final OutputStream out) throws IOException  
  116.    {  
  117.       out.write(label.getBytes());  
  118.       out.write(": ".getBytes());  
  119.       out.write(NEW_LINE.getBytes());  
  120.       for ( NavigableMap.Entry<Integer,Movie> entry :  
  121.             moviesNavigableMap.entrySet())  
  122.       {  
  123.          out.write("\t#".getBytes());  
  124.          out.write(entry.getKey().toString().getBytes());  
  125.          out.write(": ".getBytes());  
  126.          out.write(entry.getValue().getTitle().getBytes());  
  127.          out.write(NEW_LINE.getBytes());  
  128.       }  
  129.       out.write(NEW_LINE.getBytes());  
  130.    }  
  131.   
  132.    /** 
  133.     * Write demonstration of several NavigableMap methods to provided 
  134.     * OutputStream. 
  135.     *  
  136.     * @param out OutputStream to which to write output of examples using 
  137.     *    NavigableMap. 
  138.     * @throws java.io.IOException Thrown if output cannot be written to the 
  139.     *    provided OutputStream. 
  140.     */  
  141.    public void demonstrateNavigableMap(final OutputStream out) throws IOException  
  142.    {  
  143.       printHeader("Select NavigableMap Methods", out);  
  144.   
  145.       // Demonstrate NavigableMap.firstEntry()  
  146.       printNavigableMapApproach(  
  147.          "First Movie [firstEntry()]",  
  148.          favoriteMovies.firstEntry().getValue().getTitle(),  
  149.          out);  
  150.   
  151.       // Demonstrate NavigableMap.lastEntry()  
  152.       printNavigableMapApproach(  
  153.          "Last Movie [lastEntry()]",  
  154.          favoriteMovies.lastEntry().getValue().getTitle(),  
  155.          out);  
  156.   
  157.       // Demonstrate NavigableMap.floorEntry  
  158.       printNavigableMapApproach(  
  159.          "Floor Entry for '6' [floorEntry(K)]",  
  160.          favoriteMovies.floorEntry(6).getValue().getTitle(),  
  161.          out);  
  162.   
  163.       // Demonstrate NavigableMap.ceilingEntry  
  164.       printNavigableMapApproach(  
  165.          "Ceiling Entry for '3' [ceilingEntry(K)]",  
  166.          favoriteMovies.ceilingEntry(3).getValue().getTitle(),  
  167.          out);  
  168.   
  169.       // Demonstrate NavigableMap.headMap:  
  170.       //    First (lowest) entry automatically assumed; 3 is inclusive (true)  
  171.       final NavigableMap<Integer, Movie> topMovies =  
  172.          this.favoriteMovies.headMap(3true);  
  173.       printMoviesNavigableMap(  
  174.          "Top range of movies '1' through '3' [inclusive] - headMap(K,Boolean)",  
  175.          topMovies,  
  176.          out);  
  177.   
  178.       // Demonstrate NavigableMap.subMap:  
  179.       //    Make lower (from) number inclusive (true) and higher (to) number  
  180.       //    exclusive (false).  
  181.       final NavigableMap<Integer, Movie> middleMovies =  
  182.          this.favoriteMovies.subMap(4true7false);  
  183.       printMoviesNavigableMap(  
  184.            "Middle range of movies '4' through '6' [7 not inclusive] - subMap"  
  185.          + "(K,Boolean,K,Boolean)",  
  186.          middleMovies,  
  187.          out);  
  188.   
  189.       // Demonstrate NavigableMap.tailMap:  
  190.       //    Last (highest) entry automatically assumed; 6 is exclusive (false)  
  191.       final NavigableMap<Integer, Movie> bottomOfBestMovies =  
  192.          this.favoriteMovies.tailMap(6false);  
  193.       printMoviesNavigableMap(  
  194.            "Bottom range of best movies '7' through '10' [6 not inclusive] "  
  195.          + "- tailMap(K,Boolean)",  
  196.          bottomOfBestMovies,  
  197.          out);  
  198.    }  
  199.   
  200.    /** 
  201.     * Set up an instance of me with pre-populated data. 
  202.     *  
  203.     * @return Instance of me with pre-populated data (for testing and 
  204.     *    demonstration of this class). 
  205.     */  
  206.    public static FavoriteMovies setUpFavoriteMovies()  
  207.    {  
  208.       final FavoriteMovies movies = new FavoriteMovies();  
  209.       movies.addMovie(  
  210.          3,  
  211.          new Movie.Builder().title("Raiders of the Lost Ark")  
  212.                             .genre(MovieGenre.FANTASY)  
  213.                             .build());  
  214.       movies.addMovie(  
  215.          2,  
  216.          new Movie.Builder().title("Star Wars: The Empire Strikes Back")  
  217.                             .genre(MovieGenre.SCIENCE_FICTION)  
  218.                             .build());  
  219.       movies.addMovie(  
  220.          4,  
  221.          new Movie.Builder().title("Men in Black")  
  222.                             .genre(MovieGenre.SCIENCE_FICTION)  
  223.                             .build());  
  224.       movies.addMovie(  
  225.          1,  
  226.          new Movie.Builder().title("Fletch")  
  227.                             .genre(MovieGenre.COMEDY)  
  228.                             .build());  
  229.       movies.addMovie(  
  230.          5,  
  231.          new Movie.Builder().title("Ocean's Eleven")  
  232.                             .genre(MovieGenre.ACTION)  
  233.                             .build());  
  234.       movies.addMovie(  
  235.          9,  
  236.          new Movie.Builder().title("The Outlaw Josey Wales")  
  237.                             .genre(MovieGenre.WESTERN)  
  238.                             .build());  
  239.       movies.addMovie(  
  240.          8,  
  241.          new Movie.Builder().title("Groundhog Day")  
  242.                             .genre(MovieGenre.COMEDY)  
  243.                             .build());  
  244.       movies.addMovie(  
  245.          10,  
  246.          new Movie.Builder().title("The Sixth Sense")  
  247.                             .genre(MovieGenre.HORROR)  
  248.                             .build());  
  249.       movies.addMovie(  
  250.          7,  
  251.          new Movie.Builder().title("War Games")  
  252.                             .genre(MovieGenre.SCIENCE_FICTION)  
  253.                             .build());  
  254.       movies.addMovie(  
  255.          6,  
  256.          new Movie.Builder().title("The Princess Bride")  
  257.                             .genre(MovieGenre.COMEDY)  
  258.                             .build());  
  259.       return movies;  
  260.    }  
  261.   
  262.    /** 
  263.     * Main executable to demonstrate NavigableMap. 
  264.     *  
  265.     * @param arguments Command-line arguments; none anticipated. 
  266.     */  
  267.    public static void main(final String[] arguments)  
  268.    {  
  269.       final FavoriteMovies movies = setUpFavoriteMovies();  
  270.       try  
  271.       {  
  272.          movies.printContents(System.out);  
  273.          movies.demonstrateNavigableMap(System.out);  
  274.       }  
  275.       catch (Exception ex)  
  276.       {  
  277.          System.err.println("Exception encountered: " + ex.toString());  
  278.       }  
  279.    }  
  280. }  



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

  1. package dustin.examples.navigable;  
  2.   
  3. /** 
  4.  * Class representating a movie. 
  5.  *  
  6.  * @author Dustin 
  7.  */  
  8. public class Movie  
  9. {  
  10.    /** Title of the movie. */  
  11.    private String title;  
  12.   
  13.    /** Year of movie's release. */  
  14.    private int year;  
  15.   
  16.    /** Movie's primary director. */  
  17.    private String director;  
  18.   
  19.    /** Movie's primary genre. */  
  20.    private MovieGenre genre;  
  21.   
  22.    /** No-arguments constructor not intended for public consumption. */  
  23.    private Movie() {}  
  24.   
  25.    /** 
  26.     * Provide my director. 
  27.     *  
  28.     * @return My director. 
  29.     */  
  30.    public String getDirector()  
  31.    {  
  32.       return this.director;  
  33.    }  
  34.   
  35.    /** 
  36.     * Provide my genre. 
  37.     *  
  38.     * @return My genre. 
  39.     */  
  40.    public MovieGenre getGenre()  
  41.    {  
  42.       return this.genre;  
  43.    }  
  44.   
  45.    /** 
  46.     * Provide my title. 
  47.     *  
  48.     * @return My title. 
  49.     */  
  50.    public String getTitle()  
  51.    {  
  52.       return this.title;  
  53.    }  
  54.   
  55.    /** 
  56.     * Provide my rear of release. 
  57.     *  
  58.     * @return The year of my release. 
  59.     */  
  60.    public int getYear()  
  61.    {  
  62.       return this.year;  
  63.    }  
  64.   
  65.    /** 
  66.     * Provide a String representation of me. 
  67.     *  
  68.     * @return My String representation. 
  69.     */  
  70.    @Override  
  71.    public String toString()  
  72.    {  
  73.       final StringBuilder builder = new StringBuilder();  
  74.       builder.append("Title: ").append(this.title);  
  75.       builder.append("; Year: ").append(this.year);  
  76.       builder.append("; Director: ").append(this.director);  
  77.       builder.append("; Genre: ").append(this.genre);  
  78.       return builder.toString();  
  79.    }  
  80.   
  81.    public static class Builder  
  82.    {  
  83.       private String title;  
  84.       private int year;  
  85.       private String director;  
  86.       private MovieGenre genre;  
  87.   
  88.       public Builder() {}  
  89.   
  90.       public Builder director(final String newDirector)  
  91.       {  
  92.          this.director = newDirector;  
  93.          return this;  
  94.       }  
  95.   
  96.       public Builder genre(final MovieGenre newGenre)  
  97.       {  
  98.          this.genre = newGenre;  
  99.          return this;  
  100.       }  
  101.   
  102.       public Builder title(final String newTitle)  
  103.       {  
  104.          this.title = newTitle;  
  105.          return this;  
  106.       }  
  107.   
  108.       public Builder year(int newYear)  
  109.       {  
  110.          this.year = newYear;  
  111.          return this;  
  112.       }  
  113.   
  114.       public Movie build()  
  115.       {  
  116.          return new Movie(this);  
  117.       }  
  118.    }  
  119.   
  120.    /** 
  121.     * Constructor intended to be used in conjunction with Builder. 
  122.     *  
  123.     * @param builder Builder for building an instance of me. 
  124.     */  
  125.    private Movie(final Builder builder)  
  126.    {  
  127.       this.title = builder.title;  
  128.       this.director = builder.director;  
  129.       this.year = builder.year;  
  130.       this.genre = builder.genre;  
  131.    }  
  132. }  



MovieGenre.java

  1. package dustin.examples.navigable;  
  2.   
  3. /** 
  4.  * Represent a movie genre. 
  5.  *  
  6.  * @author Dustin 
  7.  */  
  8. public enum MovieGenre  
  9. {  
  10.    ACTION,  
  11.    COMEDY,  
  12.    DRAMA,  
  13.    FANTASY,  
  14.    HORROR,  
  15.    ROMANTIC_COMEDY,  
  16.    SCIENCE_FICTION,  
  17.    WESTERN  
  18. }  



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.