Java 8 — Map and the Unknown “Niceties”

Posted by & filed under , .

DukeTubbingSmallDespite Java 9 making its way into the real world nowadays, I see a lot of Java code out there still relying on the old-style Java 6-like syntax. And I’m not talking about the usage of lambda’s, stream’s and the likes, but rather some of the improvements Java 8 brought to existing classes in the JDK, to which a lot of people are still oblivious. (I blame partly this on the media which always shifts focus with each JDK release to the new packages added or changes to the language — which obscures quite often very important changes to existing classes and packages.)

In particular there are 3 additions to the Map class which Java 8 brought up which I want to address in this post; these are super-useful and make code so much nicer to write and easier to understand, as you will see.

As a reminder, the contract of the Map.get() method specifies (as per Javadoc):

Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.

This has opened the door for a lot of us to write in the past code such as this:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.get(key);
if( value == null ) {
   // ... do something here if our map doesn't contain the given key
}

This is because while the Map interface offers containsKey(), using it requires more code for the case where our value is in fact in the map:

Map<String,String> map = ...;
// ...
String key = ...;
String value;
if( map.containsKey(key) ) {
   value = map.get(key);
} else {
   // ... do something here if our map doesn't contain the given key
}

As you can see, the case where the value is present in the map will now encounter 2 calls: one for checking if the key is present in the map and one to actually retrieve the value. Since in most cases we have to always execute the branch for “key not present in the map”, we find it much easier (and quicker in terms of execution time) to write our code as shown in first listing above.

However, Java 8 adds these 2 niceties to the Map interface:

  • getOrDefault (as per Javadoc) : Returns the value to which the specified key is mapped, or defaultValue if this map contains no mapping for the key.
  • computeIfAbsentIf the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function and enters it into this map unless null.

This means now this piece of code:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.get(key);
if( value == null ) {
   value = ""; //or "default value"
}

can be written as:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.getOrDefault(key, "" /* or "default value"*/);

Less code and easier to read! (And in fact faster — read on!)

This of course only works for the case where we want to use a “default” value in the case our key does not exist in the map; there are however cases where we want to compute a “default value” (based on some contextual values) — in which case the above getOrDefault() doesn’t help much. So in these cases we can use computeIfAbsent() — which gets the key passed in as a parameter.

So this code:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.get(key);
if( value == null ) {
   value = someOtherComponent.retrieveFullObjectFromRemoteService("prefix/" + key); 
}

now becomes:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.computeIfAbsent(key, (key) -> someOtherComponent.retrieveFullObjectFromRemoteService("prefix/" + key));

Note the usages of lambda’s in the code above which makes the code even more succinct — yet nevertheless easier to read.

Bonus Points: computeIfPresent() !

If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and its current mapped value.

In other words, this allows us to retrieve the value and “filter” it through a function and retrieve the result of that “filtering”. Might sound like not much at first glance but check this out:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.get(key);
if( value != null ) {
   value = value.toUpperCase();
}

So we want to ensure all the strings we retrieve from our Map are upper case. Now using computeIfPresent transforms the above code to this:

Map<String,String> map = ...;
// ...
String key = ...;
String value = map.computeIfPresent(key, (key, value) -> value.toUpperCase());

Sweet! 🙂

 

Consideration on Performance

I decided to have a look at the speed of executing these methods versus original code shown. For this, I’ve set up a github repo with the code here: https://github.com/liviutudor/Java8MapImprovements.

While the computations are a bit simple, the basic mechanism is as follows: run some code using the pre-Java8 approach (as shown above) N times (where N is rather large so we eliminate spikes) and then using it’s Java8 counter-part, then look at the 95 percentile in terms of execution time. (In this particular case we are going to measure nano-second level time deltas as the “operations” we chose in the code are rather simple so millisecond granularity might not be enough.)

The code dumps out the timing to console so you can pipe it to a .csv file and then open it in Excel. The results from my own measurements are actually included in the Github repo: https://github.com/liviutudor/Java8MapImprovements/blob/master/output.xlsx (or if you prefer the “naked” CSV version: https://github.com/liviutudor/Java8MapImprovements/blob/master/output.csv).

getOrDefault

Pre-Java 8 Code for this looks like this (though see https://github.com/liviutudor/Java8MapImprovements/blob/master/src/main/java/GetOrDefaultPreJava8.java and https://github.com/liviutudor/Java8MapImprovements/blob/master/src/main/java/GetOrDefaultJava8.java):

...
String value = getValue(keyFound);
String valNotFound = getValue(keyNotFound);
...
 
String getValue(String key) {
   String value = map.get(key);
   if (value == null) {
      value = BaseTest.DEF;
   }
   return value;
}

Whereas the Java 8 version is:

...
String value = map.getOrDefault(keyFound, BaseTest.DEF);
String valNotFound = map.getOrDefault(keyNotFound, BaseTest.DEF);
...

Looking at the results, the Java 8 approach wins with nearly half the timing of the pre-Java 8 based approach: 335 ns versus 646 ns.

ComputeIfAbsent

For this, the “compute” operation I have selected to be a simple concatenation of the key with a constant string:

value = key + " / " + BaseTest.DEF

As such, the pre-Java 8 approach reads:

...
String value = getValue(keyFound);
String valNotFound = getValue(keyNotFound);
...
 
String getValue(String key) {
     String value = map.get(key);
     if (value == null) {
         value = key + " / " + BaseTest.DEF;
     }
     return value;
}

Versus having a method getValueForNotFound in the Java 8 based class and using it in the call to computeIfAbsent:

...
String value = map.computeIfAbsent(keyFound, this::getValueForNotFound);
String valNotFound = map.computeIfAbsent(keyNotFound, this::getValueForNotFound);
...
String getValueForNotFound(String key) {
        return key + " / " + BaseTest.DEF;
}

Looking at the results, again the Java 8 approach wins with less than half the timing of the pre-Java 8 based approach: 707 ns versus 1,622 ns.

computeIfPresent

For this, I have chosen the “computation” to be upper-case’ing the given string — pretty much in line with the samples above. So the pre-Java 8 code is:

...
String value = getValue(keyFound);
String valNotFound = getValue(keyNotFound);
...
 
String getValue(String key) {
        String value = map.get(key);
        if (value != null) {
            value = value.toUpperCase();
        }
        return value;
}

And for Java 8 again we define a getUppercase method and pass that to computeIfPresent:

...
String value = map.computeIfPresent(keyFound, this::getUppercase);
String valNotFound = map.computeIfPresent(keyNotFound, this::getUppercase);
...
String getUppercase(String key, String value) {
        return value.toUpperCase();
}

The results on this show Java 8 approach is faster than the pre-Java 8 code: 2,093 ns vs 3,330 ns.

Overall I think we can agree that the Java 8 approach is a better one … in all aspects, right?