At Data Geekery, we love Java. And as we’re really into jOOQ’s fluent API and query DSL, we’re absolutely thrilled about what Java 8 will bring to our ecosystem. We have blogged a couple of times about some nice Java 8 goodies, and now we feel it’s time to start a new blog series, the…
Java 8 Friday
Every Friday, we’re showing you a couple of nice new tutorial-style Java 8 features, which take advantage of lambda expressions, extension methods, and other great stuff. You’ll find the source code on GitHub.
Java 8 Goodie: Map Enhancements
In previous posts, we’ve already dealt with a couple of new Streams features, for instance when sorting. Most API improvements are indeed part of the new Streams API. But a few nice methods were also added to java.util.List
and most importantly, to java.util.Map
. If you want a quick overview of feature additions, go to the JDK8 Javadoc and click on the new “Default Methods” tab:

For backwards-compatibility reasons, all new methods added to Java interfaces are in fact default methods. So we have a couple of exciting new additions!
compute() methods
Often, we fetch a value from a map, make some calculations on it and put it back into the map. This can be verbose and hard to get right if concurrency is involved. With Java 8, we can pass a BiFunction
to the new compute()
, computeIfAbsent()
, or computeIfPresent()
methods and have the Map
implementation handle the semantics of replacing a value.
The following example shows how this works:
// We'll be using this simple map // Unfortunately, still no map literals in Java 8.. Map<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2); map.put("C", 3); // Compute a new value for the existing key System.out.println(map.compute("A", (k, v) -> v == null ? 42 : v + 41)); System.out.println(map); // This will add a new (key, value) pair System.out.println(map.compute("X", (k, v) -> v == null ? 42 : v + 41)); System.out.println(map);
The output of the above program is this:
42 {A=42, B=2, C=3} 42 {A=42, B=2, C=3, X=42}
This is really useful for ConcurrentHashMap
, which ships with the following guarantee:
The entire method invocation is performed atomically. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this Map.
forEach() method
This is a really nice goodie which lets you pass a method reference or a lambda to receive (key, value) pairs one by one. A trivial example would be this:
map.forEach((k, v) -> System.out.println(k + "=" + v));
Its output being:
A=1 B=2 C=3
merge() method
Now this one is really not so easy to understand. The Javadoc uses this example here:
map.merge(key, msg, String::concat)
Given the following contract:
If the specified key is not already associated with a value or is associated with null, associates it with the given value. Otherwise, replaces the value with the results of the given remapping function, or removes if the result is null.
So, the above code translates to the following atomic operation:
String value = map.get(key); if (value == null) map.put(key, msg); else map.put(key, value.concat(msg));
This is certainly not an everyday functionality and might just have leaked from an implementation to the top-level API. Additionally, if the map already contains null
(so, null
values are OK), and your remappingFunction
returns null
, then the entry is removed. That’s quite unexpected. Consider the following program:
map.put("X", null); System.out.println(map.merge( "X", null, (v1, v2) -> null)); System.out.println(map);
Its output is:
null {A=1, B=2, C=3}
Update: I first wrote the above code first with JDK 8 build 116. With build 129, things have changed completely again. First off, the value passed to merge()
is not allowed to be null
. Secondly. null
values are treated by merge()
just like absent values. To produce the same output, we’ll write:
map.put("X", 1); System.out.println(map.merge( "X", 1, (v1, v2) -> null)); System.out.println(map);
This merge()
operation has thus removed a value from the map. That’s probably OK because the semantics of “merge” is often a combination of INSERT
, UPDATE
, and DELETE
if we’re using SQL-speak. And a somewhat reasonable way to indicate that a value should be removed is to return null
from such a function.
But the map is allowed to contain null
values, which can never be inserted into the map using merge()
.
getOrDefault()
This is a no-brainer. Right? Right! Wrong!
Unfortunately, there are two types of Maps. Those supporting null
keys and/or values and those who don’t support nulls
. While the previous merge()
method didn’t distinguish between a map not containing a key and a map containing a key with a null
value, this new getOrDefault()
only returns the default when the key is not contained. It won’t protect you from a NullPointerException
:
map.put("X", null); try { System.out.println(map.getOrDefault("X", 21) + 21); } catch (NullPointerException nope) { nope.printStackTrace(); }
That’s quite a bummer. In general, it can be said the Map API has become even more complex with respect to nulls.
Trivial additions
There are a few more methods, like putIfAbsent()
(pulled up from ConcurrentHashMap
, remove()
(with key and value arguments), replace()
.
Conclusion
All in all, it can be said that a lot of atomic operations have made it to the top-level Map API, which is good. But then again, the pre-existing confusion related to the semantics of null
in maps has deepened. The terminologies “present” vs. “absent”, “contains”, “default” don’t necessarily help clarifying these things, which is surprisingly against the rules of keeping an API consistent and most importantly, regular. Thus as a consumer of this API, ideally, you should keep null
out of maps, both as keys and as values!
Next week in this blog series, we’re going to look at how Java 8 will allow you to define local transactional scope very easily, so stay tuned!
More on Java 8
In the mean time, have a look at Eugen Paraschiv’s awesome Java 8 resources page
Code sample in “merge() method” chapter throws NPE with Java 8 b129
Hmm, interesting. I had checked with b116. Will check again. I’m assuming you’re talking about this example here:
Yes, doc for HashMap::merge says:
If the specified key is not already associated with a value or is associated with null, associates it with the given non-null value
So second argument in merge can’t be null.
Hmm, yes you’re right. The javadoc also says:
I guess I do not yet fully understand the intent of this method. My dummy example clearly isn’t a real-world use-case.
Hmm, this method has completely changed between b116 and b129. I’ll fix yet another discovery…
// Count occurrences of string(s) in string
Java 8 does have map literals! They’re just hidden behind lambda expressions.Try something like this (https://gist.github.com/galdosd/10823529) to get a nice syntax like this:
Very nice idea, thank you very much for the contribution!