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:
java.util.Map default methods
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:
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:
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. nullvalues are treated by merge() just like absent values. To produce the same output, we’ll write:
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:
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!
value – the non-null value to be merged with the existing value associated with the key or, if no existing value or a null value is associated with the key, to be associated with the key
I guess I do not yet fully understand the intent of this method. My dummy example clearly isn’t a real-world use-case.
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:
Map m = hashMap(
bob -> 5,
TheGimp -> 8,
incredibleKoolAid -> "James Taylor",
heyArnold -> new Date()
);
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!