Thursday, April 17, 2014

Handy New Map Default Methods in JDK 8

The Map interface provides some handy new methods in JDK 8. Because the Map methods I cover in this post are implemented as default methods, all existing implementations of the Map interface enjoy the default behaviors defined in the default methods without any new code. The JDK 8 introduced Map methods covered in this post are getOrDefault(Object, V), putIfAbsent(K, V), remove(Object, Object), remove(Object, Object), replace(K, V), and replace(K, V, V).

Example Map for Demonstrations

I will be using the Map declared and initialized as shown in the following code throughout the examples in this blog post. The statesAndCapitals field is a class-level static field. I intentionally have only included a small subset of the fifty states in the United States for reading clarity and to allow easier demonstration of some of the new JDK 8 Map default methods.

   private final static Map statesAndCapitals;

   static
   {
      statesAndCapitals = new HashMap<>();
      statesAndCapitals.put("Alaska", "Anchorage");
      statesAndCapitals.put("California", "Sacramento");
      statesAndCapitals.put("Colorado", "Denver");
      statesAndCapitals.put("Florida", "Tallahassee");
      statesAndCapitals.put("Nevada", "Las Vegas");
      statesAndCapitals.put("New Mexico", "Sante Fe");
      statesAndCapitals.put("Utah", "Salt Lake City");
      statesAndCapitals.put("Wyoming", "Cheyenne");
   }
Map.getOrDefault(Object, V)

Map's new method getOrDefault(Object, V) allows the caller to specify in a single statement to get the value of the map that corresponds to the provided key or else return a provided "default value" if no match is found for the provided key.

The next code listing compares how checking for a value matching a provided key in a map or else using a default if no match is found was implemented before JDK 8 and how it can now be implemented with JDK 8.

/*
 * Demonstrate Map.getOrDefault and compare to pre-JDK 8 approach. The JDK 8
 * addition of Map.getOrDefault requires fewer lines of code than the
 * traditional approach and allows the returned value to be assigned to a
 * "final" variable.
 */

// pre-JDK 8 approach
String capitalGeorgia = statesAndCapitals.get("Georgia");
if (capitalGeorgia == null)
{
   capitalGeorgia = "Unknown";
}

// JDK 8 approach
final String capitalWisconsin = statesAndCapitals.getOrDefault("Wisconsin", "Unknown");

The Apache Commons class DefaultedMap provides functionality similar to the new Map.getOrDefault(Object, V) method. The Groovy GDK includes a similar method for Groovy, Map.get(Object, Object), but that one's behavior is a bit different because it not only returns the provided default if the "key" is not found, but also adds the key with the default value to the underlying map.

Map.putIfAbsent(K, V)

Map's new method putIfAbsent(K, V) has Javadoc advertising its default implementation equivalent:

The default implementation is equivalent to, for this map:
 
 V v = map.get(key);
 if (v == null)
     v = map.put(key, value);

 return v;

This is illustrated with another code sample that compares the pre-JDK 8 approach to the JDK 8 approach.

/*
 * Demonstrate Map.putIfAbsent and compare to pre-JDK 8 approach. The JDK 8
 * addition of Map.putIfAbsent requires fewer lines of code than the
 * traditional approach and allows the returned value to be assigned to a
 * "final" variable.
 */

// pre-JDK 8 approach
String capitalMississippi = statesAndCapitals.get("Mississippi");
if (capitalMississippi == null)
{
   capitalMississippi = statesAndCapitals.put("Mississippi", "Jackson");
}

// JDK 8 approach
final String capitalNewYork = statesAndCapitals.putIfAbsent("New York", "Albany");

Alternate solutions in the Java space before the addition of this putIfAbsent method are discussed in the StackOverflow thread Java map.get(key) - automatically do put(key) and return if key doesn't exist?. It's worth noting that before JDK 8, the ConcurrentMap interface (extends Map) already provided a putIfAbsent(K, V) method.

Map.remove(Object, Object)

Map's new remove(Object, Object) method goes beyond the long-available Map.remove(Object) method to remove a map entry only if both the provided key and provided value match an entry in the map (the previously available version only looked for a "key" match to remove).

The Javadoc comment for this method explains the how the default method's implementation works in terms of equivalent pre-JDK 8 Java code:

The default implementation is equivalent to, for this map:
if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
     map.remove(key);
     return true;
 } else
     return false;

A concrete comparison of the new approach to the pre-JDK 8 approach is shown in the next code listing.

/*
 * Demonstrate Map.remove(Object, Object) and compare to pre-JDK 8 approach.
 * The JDK 8 addition of Map.remove(Object, Object) requires fewer lines of
 * code than the traditional approach and allows the returned value to be
 * assigned to a "final" variable.
 */

// pre-JDK 8 approach
boolean removed = false;
if (   statesAndCapitals.containsKey("New Mexico")
    && Objects.equals(statesAndCapitals.get("New Mexico"), "Sante Fe"))
{
   statesAndCapitals.remove("New Mexico", "Sante Fe");
   removed = true;
}

// JDK 8 approach
final boolean removedJdk8 = statesAndCapitals.remove("California", "Sacramento");
Map.replace(K, V)

The first of the two new Map "replace" methods sets the specified value to be mapped to the specified key only if the specified key already exists with some mapped value. The Javadoc comment explains the Java equivalent of this default method implementation:

The default implementation is equivalent to, for this map:
 if (map.containsKey(key)) {
     return map.put(key, value);
 } else
     return null;

The comparison of this new approach to the pre-JDK 8 approach is shown next.

/*
 * Demonstrate Map.replace(K, V) and compare to pre-JDK 8 approach. The JDK 8
 * addition of replace(K, V) requires fewer lines of code than the traditional
 * approach and allows the returned value to be assigned to a "final" 
 * variable.
 */

// pre-JDK 8 approach
String replacedCapitalCity;
if (statesAndCapitals.containsKey("Alaska"))
{
   replacedCapitalCity = statesAndCapitals.put("Alaska", "Juneau");
}

// JDK 8 approach
final String replacedJdk8City = statesAndCapitals.replace("Alaska", "Juneau");
Map.replace(K, V, V)

The second newly added Map "replace" method is more narrow in its interpretation of which existing values are replaced. While the method just covered replaces any value in a value available for the specified key in the mapping, this "replace" method that accepts an additional (third) argument will only replace the value of a mapped entry that has both a matching key and a matching value. The Javadoc comment shows the default method's implementation:

The default implementation is equivalent to, for this map:
 
 if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
     map.put(key, newValue);
     return true;
 } else
     return false;

My comparison of this approach to the pre-JDK 8 approach is shown in the next code listing.

/*
 * Demonstrate Map.replace(K, V, V) and compare to pre-JDK 8 approach. The
 * JDK 8 addition of replace(K, V, V) requires fewer lines of code than the
 * traditional approach and allows the returned value to be assigned to a
 * "final" variable.
 */

// pre-JDK 8 approach
boolean replaced = false;
if (   statesAndCapitals.containsKey("Nevada")
    && Objects.equals(statesAndCapitals.get("Nevada"), "Las Vegas"))
{
   statesAndCapitals.put("Nevada", "Carson City");
   replaced = true;
}

// JDK 8 approach
final boolean replacedJdk8 = statesAndCapitals.replace("Nevada", "Las Vegas", "Carson City");
Observations and Conclusion

There are several observations to make from this post.

  • The Javadoc methods for these new JDK 8 Map methods are very useful, especially in terms of describing how the new methods behave in terms of pre-JDK 8 code. I discussed these methods' Javadoc in a more general discussion on JDK 8 Javadoc-based API documentation.
  • As the equivalent Java code in these methods' Javadoc comments indicates, these new methods do not generally check for null before accessing map keys and values. Therefore, one can expect the same issues with nulls using these methods as one would find when using "equivalent" code as shown in the Javadoc comments. In fact, the Javadoc comments generally warn about the potential for NullPointerException and issues related to some Map implementations allowing null and some not for keys and values.
  • The new Map methods discussed in this post are "default methods," meaning that implementations of Map "inherit" these implementations automatically.
  • The new Map methods discussed in this post allow for cleaner and more concise code. In most of my examples, they allowed the client code to be converted from multiple state-impacting statements to a single statement that can set a local variable once and for all.

The new Map methods covered in this post are not ground-breaking or earth-shattering, but they are conveniences that many Java developers previously implemented more verbose code for, wrote their own similar methods for, or used a third-party library for. JDK 8 brings these standardized methods to the Java masses without need for custom implementation or third-party frameworks. Because default methods are the implementation mechanism, even Map implementations that have been around for quite a while suddenly and automatically have access to these new methods without any code changes to the implementations.

No comments: