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 ofMap
“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.
Reference: | Handy New Map Default Methods in JDK 8 from our JCG partner Dustin Marx at the Inspired by Actual Events blog. |