Introduction

The default methods was added in Java 8. Many JDK interfaces were enhanced with default methods. In this post we will learn about the default methods in the Java Map interface.

Default methods in Java Map interface

The following default methods were added to the Map interface in Java 8:

  • GetOrDefault
  • ForEach
  • Remove
  • Replace and ReplaceAll
  • PutIfAbsent
  • Merge
  • Compute
  • ComputeIfAbsent
  • ComputeIfPresent

GetOrDefault

We pass a key and a default value to the getOrDefault method. It returns the value mapped to the key in the map (if present) or it returns the default value passed.

This is useful to avoid if checks - to check first if the key is present in the map using containsKey.

Map<Integer, String> map = Map.of(1, "a", 2, "b", 3, "c");

System.out.println(map.getOrDefault(1, "N/A")); //a
System.out.println(map.getOrDefault(2, "N/A")); //b
System.out.println(map.getOrDefault(4, "N/A")); //N/A

The first two calls would return values a and b as keys 1 and 2 are present in the map. The last call queries the map for the key 4, which is not present in the map. Hence, it will return the default value we passed.

Note: The map was created using the static factory method of in the Map interface (which was added in Java 9). Refer to the Convenience Factory Methods for Collections post to learn more about them.

ForEach

The forEach method accepts a BiConsumer (a functional interface accepting two values) which does some action on the passed values. The default implementation passes each entry (key and the value) in the map to this BiConsumer. As an example, we can use this to print the map in a custom format.

Map<Integer, String> map = Map.of(1, "a", 2, "b", 3, "c");

map.forEach((k, v) -> System.out.println(k + ": " + v));

Prints,

3: c
2: b
1: a

Remove

The remove method allows us to do conditional removal. It removes an entry for the specified key only if it is currently mapped to the specified value. In other words, it accepts a key and a value and removes the entry for the key only if it is currently mapped to the passed value. The operation returns a boolean indicating if there was a removal or not.

Map<Integer, String> map = new HashMap<>(Map.of(1, "a", 2, "b", 3, "c"));
System.out.println(map.remove(1, "a")); //true
System.out.println(map.remove(2, "value")); //false
System.out.println(map); // {2=b, 3=c}

The first call will remove the entry with key 1 as it is mapped to value “a". The second call asks the map to remove the entry for the key 2 if it is mapped to the value “value”. Since it is false, the entry will not be removed. The final map has entries for keys 2 and 3 as shown.

Replace

There are two versions of the replace method.

Replace if the key is mapped to a value

The first version of the replace method accepts a key and a value. It will replace the entry for the specified key only if it is currently mapped to some value. In other words, map.containsKey(key) must be true for the replacement operation to take place. If the key is not mapped to any value, replacement is not done. The call returns the previous value mapped to the key.

Map<Integer, String> map = new HashMap<>(Map.of(1, "a", 2, "b", 3, "c"));
System.out.println(map.replace(1, "1-a")); //a
System.out.println(map.replace(4, "4-d")); //null

System.out.println(map); //{1=1-a, 2=b, 3=c}

The first call calls the replace method passing key as 1 and value as 1-a. Since key 1 is mapped to some value, it will replace it with the passed value. The operation would return the previously mapped value (which is a). In the second call, since key 4 is not present in the map, the replace call does not modify the map. The call returns null. The final map has keys 1, 2 and 3 as shown above.

Replace if the key is mapped to the specified value

This overloaded replace method is an extension of the above version. It accepts three arguments:

  • key
  • expected current value
  • new value

It replaces the entry for the specified key only if the key is mapped to the specified value. It returns a boolean result to indicate if the value was replaced or not.

Map<Integer, String> map = new HashMap<>(Map.of(1, "a", 2, "b", 3, "c"));
System.out.println(map.replace(1, "a", "1-a")); //true
System.out.println(map.replace(2, "value", "2-b")); //false
System.out.println(map.replace(4, "value", "4-d")); //false
System.out.println(map); //{1=1-a, 2=b, 3=c}

The first call replaces the value for the key 1 with 1-a if it is currently mapped to the value a. Since this is true, the map entry for the key 1 is replaced with the new value and it returns true.

The second call asks to replace the value for key 2 if its current value is “value”. Since this is false, it does not replace the value for the key 2.

The third call uses replace on a non-existent key and hence no replacement happens. The final map output is also printed.

ReplaceAll

The replaceAll method accepts a BiFunction (BiFunction<? super K, ? super V, ? extends V>) where the first argument is the key, the second argument is the value and it returns a new value.

For each entry in the map, it invokes this BiFunction and replaces the value with the new value returned by this function.

Example: We have a map and we replace the value for all the entries by prepending the key and a hyphen to the current value.

Map<Integer, String> map = new HashMap<>(Map.of(1, "a", 2, "b", 3, "c"));
System.out.println(map); //{1=a, 2=b, 3=c}

map.replaceAll((k, v) -> k + "-" + v);
System.out.println(map); //{1=1-a, 2=2-b, 3=3-c}

PutIfAbsent

The putIfAbsent method accepts a key and a value. If the passed key is not mapped to a value, it associates the passed value with it. It returns the previously associated value with the key. Hence,

  • If the key was not present in the map, it maps the passed value to the key and returns null.
  • Otherwise, it does nothing and returns the currently mapped value.
Map<Integer, String> map = new HashMap<>(Map.of(1, "a", 2, "b", 3, "c"));

System.out.println(map.putIfAbsent(4, "d")); //null
System.out.println(map.putIfAbsent(3, "3-c")); //c

System.out.println(map); //{1=a, 2=b, 3=c, 4=d}

The first call puts an entry {4=d} into the map if key 4 is not present. Since 4 is not mapped to any value, it performs the update and returns null. In the second call, since 3 is already mapped to some value, no updates are done. It simply returns the existing value for the key 3.

Merge

The merge method accepts a key, a value and a BiFunction which is a remapping function.

The signature of the merge method is:

default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction)

If the passed key is not mapped to any value, it maps the key to the passed value. Otherwise, it invokes the remapping function by passing the existing value, and the passed (new) value and updates the mapping with the result of the function. If the function returns a null, it removes the mapping. The merge method returns the new value associated with the passed key.

Note: The remapping function should not modify the map during the computation.

Map<String, Integer> count = new HashMap<>(Map.of("a", 1, "b", 1));

System.out.println(count.merge("c", 1, Integer::sum)); //1
//or
//System.out.println(count.merge("c", 1, (oldVal, newVal) -> oldVal + newVal));

System.out.println(count); //{a=1, b=1, c=1}

We call the merge method with key as c and value as 1. We pass a BiFunction, which takes the old value and the new value and adds them. This was represented by a method reference. In this case, since the map does not have any entry for the key c, it maps it to the value 1. Now the map has entries for keys a, b and c - all mapped to value 1.

Calling merge again with the same set of arguments, changes the value for the key c.

System.out.println(count.merge("c", 1, Integer::sum)); //2
System.out.println(count); //{a=1, b=1, c=2}

This time, since key c is present in the map, the passed BiFunction is invoked with values 1 (the existing value) and 1 (the new value). The function adds them and returns 2, which is then mapped to the key c. The call to merge returns this value.

Let us pass value 3 this time. Now the BiFunction adds 2 (existing value) and 3 (passed value) and the value for the key c is updated as 5.

System.out.println(count.merge("c", 3, Integer::sum)); //5
System.out.println(count); //{a=1, b=1, c=5}

If the BiFunction returns a null, it will remove the entry from the map. In the below code snippet, since we return a null, the entry for key c will be removed from the map.

System.out.println(count.merge("c", 2, (oldVal, newVal) -> null)); //null
System.out.println(count); //{a=1, b=1}

Compute

Method signature:

default V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction)

It accepts a key and a BiFunction and calls the remapping function passing the key, the current value (could be null if the key has no mapping). It updates the mapping for the entry with the passed key with the value returned by the function. If the remapping function returns a null, it will remove the entry for the key from the map. The operation will return the new value mapped to the key.

As in the merge operation, the remapping function must not modify the map.

In the below code, we call the compute method passing key as c. The BiFunction takes a key and a value and returns 1 if the value is null (i.e., the key has no mapping) or returns value +1 otherwise.

 Map<String, Integer> count = new HashMap<>(Map.of("a", 1, "b", 1));
 System.out.println(count.compute("c", (key, value) -> value == null ? 1 : value + 1)); //1
 System.out.println(count); //{a=1, b=1, c=1}

Since c is not present in the map, it returns 1 and it is mapped to the key c.

Let us call compute again with the same set of arguments twice. I have assigned the BiFunction lambda expression to a variable so that we can reuse it.

BiFunction<String, Integer, Integer> func = (k, v) -> v == null ? 1 : v + 1;
System.out.println(count.compute("c", func)); //2
System.out.println(count.compute("c", func)); //3

System.out.println(count); //{a=1, b=1, c=3}

The first call will invoke the BiFunction with key c and value 1 and it returns 2. The second call will call the function with key c and value 2 and it returns 3. Finally, the map has value 3 for key c.

If we return a null, it will remove the entry for the key.

System.out.println(count.compute("c", (k, v) -> null)); //null
System.out.println(count); //{a=1, b=1}

ComputeIfAbsent

Method signature:

default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction)

It takes in a key and a Function. If the passed key is not mapped to any value in the map (or mapped to null), then it will call the passed function with the key and get back a value (result of the mapping function). It will update the mapping with the returned value.

If the map already has a mapping for the key, then no action will be performed. If the mapping function returns a null, it will remove the entry for the key from the map.

Note: The mapping function must not update the map during the computation.

Let us create a Map<String, List<String> as shown below.

Map<String, List<String>> map = new HashMap<>(
        Map.of(
                "a", new ArrayList<>(List.of("a-1", "a-2")),
                "b", new ArrayList<>(List.of("b-1", "b-2"))
));

Let us call computeIfAbsent with key c and pass a mapping function which will create a new ArrayList. Since the key c is not mapped to any value, the function will be called, and the returned value will be mapped to the key c.

System.out.println(map.computeIfAbsent("c", key -> new ArrayList<>())); //[]

An advantage of this method returning the current value is that we can chain calls on it like:

map.computeIfAbsent("d", key -> new ArrayList<>())
        .add("d-1");
System.out.println(map); //{d=[d-1], a=[a-1, a-2], b=[b-1, b-2], c=[]}

We called computeIfAbsent for key d which will create a new list value for it and map against it. Since it returns the newly computed value for the key, we can add value to the list as shown above. The current map values are shown above.

Calling computeIfAbsent on an existing value will not invoke the function at all. It simply returns the currently mapped value.

System.out.println(map.computeIfAbsent("d", key -> new ArrayList<>())); //[d-1]
map.computeIfAbsent("d", key -> new ArrayList<>())
        .add("d-2");
System.out.println(map); //{a=[a-1, a-2], b=[b-1, b-2], c=[], d=[d-1, d-2]}

ComputeIfPresent

Method signature:

default V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction)

It accepts a key and a BiFunction (remapping function). If the value for the passed key is present (and not null), then it calls the passed BiFunction passing the key and the current value. It updates the mapping with the returned result. If the remapping function returns null, the mapping will be removed. As earlier, the remapping function should not modify the map. The computeIfPresent operation will return the new value associated with the specified key.

Example: We call computeIfPresent for an existing key (b). The passed function takes the value (which is a list) and adds a new element to it and returns it. This call will print the latest value that is mapped to the passed key.

Map<String, List<String>> map = new HashMap<>(
        Map.of(
                "a", new ArrayList<>(List.of("a-1", "a-2")),
                "b", new ArrayList<>(List.of("b-1", "b-2"))
));

//Prints - [b-1, b-2, b-3]
System.out.println(map.computeIfPresent("b", (k, v) -> {
    v.add(k + "-3");
    return v;
}));

If we pass a key that is not present in the map, then it does nothing. This will print a null as the key c is not mapped to any value in the map.

//Prints null
System.out.println(map.computeIfPresent("c", (k, v) -> {
    v.add(k + "-1");
    return v;
}));

The final map contents are,

System.out.println(map); //{a=[a-1, a-2], b=[b-1, b-2, b-3]}

Conclusion

In this post, we have learnt about the useful default methods in the Java Map interface. Using these will make a lot of our code elegant (and simple) by avoiding if conditions.

Reference

Map Javadoc