Java 8: Declarative ways of modifying a Map using compute, merge and replace

If you are familiar with the map interface in Java, you know that put is the traditional way to add new data to a map.

But as you know, when writing your code, an update almost always comes with conditions that have to be checked.

The typical way we’ve been solving this for maps has been by creating if-statements and other imperative code to do checks against the map before actually updating it.

This type of imperative code quickly gets bloated, repetitive, and draws focus away from what we actually want done.

This has luckily come to an end with Java 8, where we’ve got a lot of new methods that work on a higher level.

Using these methods, we can write more declarative code that is easier to reason about.

To see how they really shine, let’s compare it against the traditional put by solving a few problems.

Consider the following scenario.

We have a Map<String, List<Article>> that contains tags mapping to their associated articles.

Modifying data in the map

Let’s start off by sorting the list of articles associated with the Java tag.

Using put, we would first have to get the value from the map, sort the data, then put it back in the map.

 map.put("Java", sortAlphabetically(map.get("Java")));

In Java 8, we got a new method — compute — giving us the possibility to define a function describing how we want to change the data for a given key.

map.compute("Java", (key, value) -> sortAlphabetically(value));  

Only modify if key already exists in the map

There is a problem with the solution above.

If the map doesn’t have any value associated with the key, it’ll create a NullPointerException.

The traditional way to solve this, is to wrap your put into an if-statement using containsKey.

if (map.containsKey("Java")) {  
  map.put("Java", sortAlphabetically(map.get("Java")));
}

Using Java 8, we got a more declarative approach using computeIfPresent.

map.computeIfPresent("Java", (key, value) -> sortAlphabetically(value));  

What this function does is to abstract the if-statement, leaving us with the responsibility of creating a function describing what we want done if the value is present.

Add data to a map only if key isn’t there

Moving on, we may want the opposite — setting a value only if the key doesn’t exists.

So let’s say we want to add a list of articles to the Java tag only if it doesn’t exist in the map.

Let’s first look at the old way using put and containsKey.

if (!map.containsKey("Java")) {  
  map.put("Java", javaArticles);
}

Now there are two new ways we can solve this.

First — we could use putIfAbsent.

map.putIfAbsent("Java", javaArticles);  

Again, we see how these new declarative methods make our code more concise and readable.

But as promised, there is another way we can solve this — and to see how this next solution really shines, consider the following scenario.

We’ve seen that putIfAbsent removes the imperative way of having to define the if-statement, but what if fetching the Java articles is really hurting our performance?

To optimise this, we don’t want to fetch the articles until we’re really sure we need them — meaning we need to know if the key is absent before fetching the articles.

Since putIfAbsent takes a value to put in the map, we can’t get the laziness we want.

So instead we could use another cool function introduced in the Map interface — computeIfAbsent.

map.computeIfAbsent("Java", this::heavyOperationToFetchArticles);  

By using computeIfAbsent we’re able to wrap the heavy operation within a function that only will be executed if needed.

Merging new data with existing data

To get familiar with our next method, let’s now say that we got a new list of Java articles and we want to merge them together with the already existing list in the map.

However — if our map doesn’t contain the Java tag, we just want to update the map with our new list.

Let’s start by looking at how we could have solved this using the traditional put method.

if (map.containsKey("Java")) {  
  map.get("Java").addAll(javaArticles);
} else {
  map.put("Java", javaArticles);
}

Again we see the use of the if-statement to achieve what we want.

An extra thing about this solution is that it is adding the javaArticles to the old list in a mutable way.

Let’s now see how we could solve this in a more declarative and immutable way using Java 8.

map.merge("Java", javaArticles, (list1, list2) ->  
  Stream.of(list1, list2)
    .flatMap(Collection::stream)
    .collect(Collectors.toList()));

To use the merge function we have to give the tag, the new list of articles and a BiFunction defining the merging.

Further more we’ve achieved immutability by using the new Stream API.

Replacing all values in a map

Let’s look at one last example, where we want to update the list of articles for every tag.

There are many traditional ways of doing this — one being a for-loop with a put.

for (String key : map.keys()) {  
  map.put(key, getUpdatedListFor(key));
}

By iterating the keys, we can update the value for each of the tags.

This can be simplified using replaceAll.

map.replaceAll((key, val) -> getUpdatedListFor(key));  

All we have to do is to give a BiFunction describing how you want to change you values.

Further reading

Make sure to checkout the documentation for a complete list of available methods in the Map interface.

Enjoyed the post?

If you don't want to miss future posts, make sure to subscribe