Monday, September 30, 2019

Java 8 – Map’s computeIfAbsent, computeIfPresent, getOrDefault methods

In this tutorial blog, we will be looking at the enhancements introduced in java.util.Map interface in Java 8. We will first quickly understand what are multi-value maps. Next we will create a multi-value map which will serve as the base problem set for explaining the new Map methods. We will first see the working of Java 8’s new Map.forEach() and Map.replaceAll() methods. Next, we will understand the new default methods introduced in Java 8 which simplify using multi-value maps. These methods are Map.computeIfAbsent(), Map.computeIfPresent() and Map.getOrDefault() methods.

What is a multi-value map

A multi-value map is a normal instance of java.util.Map. The only difference is that instead of having a ‘single’ value corresponding to each key, a multi-value map instead has a ‘collection’ such as a List or a Set as the value stored against each key.

Multi-value map with ArrayList as value
A good example of a use-case for a multi-value map would be of the hash table used to store hash keys and corresponding values. In a hash table, there exists a mappping between a hash value(key) and entries stored in a bucket (value) stored corresponding to it. A hash table is thus essentially a multi-value map.

Defining the data set/multi-value Map for this tutorial

Java 8 code defining a multi-value map
package com.javabrahman.java8;
public class Employee {
  private String name;
  private Integer age;
  private Double salary;
  public Employee(String name, Integer age, Double salary) {
    this.name = name;
    this.age = age;
    this.salary = salary;
  }
  public String toString(){
    DecimalFormat dformat = new DecimalFormat(".##");
    return "Employee Name:"+this.name
        +"  Age:"+this.age
        +"  Salary:"+dformat.format(this.salary);
  }
//getters and setters for name, age and salary go here
//standard equals() and hashcode() code go here
}
//MultiValueMapsExample.java
package com.javabrahman.java8.collections;
import com.javabrahman.java8.Employee;
import java.util.*;
public class MultiValueMapsExample {
  static Map<Integer, List<Employee>> employeeDOJMap = new HashMap<>();
 
  public static void main(String args[]) {

    List<Employee> list2014 = Arrays.asList(
        new Employee("Deborah Sprightly", 29, 9000.00));
    employeeDOJMap.put(2014, list2014);
    List<Employee> list2015 = Arrays.asList(
        new Employee("Tom Jones", 45, 7000.00),
        new Employee("Harry Major", 25, 10000.00));
    employeeDOJMap.put(2015, list2015);
    List<Employee> list2016 = Arrays.asList(
        new Employee("Ethan Hardy", 65, 8000.00),
        new Employee("Nancy Smith", 22, 12000.00));
    employeeDOJMap.put(2016, list2016);
  }
}

Java 8’s new Map.forEach() and Map.replaceAll() methods

Let us start by quickly going through the relatively simpler methods introduced in Map interface in Java 8- Map.forEach() and Map.replaceAll().

Map.forEach()

Map.forEach() method is defined as –

default void forEach(BiConsumer<? super K, ? super V> action)
Where,
     – action is the only parameter and is an instance of a BiConsumer functional interface, and,
     – method applies the logic provided via action to all the entries in the map as it ‘consumes’ them.



What is a BiConsumerWhat is a BiConsumer

java.util.function.BiConsumer is a functional interface, and is a two-arity specialization of a Consumer Functional Interface. I.e. it accepts two inputs as arguments and does not return any output.

Map.replaceAll()

Map.replaceAll() method is defined as –

default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
Where,
     – function is the only parameter and is an instance of a BiFunction Functional Interface, and,
     – the method applies the logic provided via function to all the values in the map and transforms them inside the map itself.

What is a BiFunctionWhat is a BiFunction

java.util.function.BiFunction is a functional interface, and is a two-arity specialization of Function. I.e. it accepts two inputs as arguments and returns a result after performing a computation with the input.

Let us now see the Map.forEach() and Map.replaceAll() methods in action –

Java 8 code showing Map.forEach() and Map.replaceAll() methods
System.out.println("Using Map.forEach to print the Employee in employeeDOJMap multi-value map\n");
employeeDOJMap.forEach((year,empList)->System.out.println(year+"-->" +empList));

System.out.println("\nCAPITALIZED Employee Names using Map.replaceAll()");
employeeDOJMap.replaceAll((year, empList) -> {
empList.replaceAll(emp -> {
    emp.setName(emp.getName().toUpperCase());
    return emp;
  });
  return empList;
});

employeeDOJMap.forEach((year, empList)-> System.out.println(year+"-->"+empList));


 OUTPUT of the above code


Using Map.forEach to print the Employee in employeeDOJMap multi-value map –
2016–>[Employee Name: Ethan Hardy  Age: 65  Salary: 8000.0, Employee Name: Nancy Smith  Age: 22  Salary: 12000.0]
2014–>[Employee Name: Deborah Sprightly  Age: 29  Salary: 9000.0]
2015–>[Employee Name: Tom Jones  Age: 45  Salary: 7000.0, Employee Name: Harry Major  Age:25  Salary: 10000.0]

CAPITALIZED Employee Names using Map.replaceAll()
2016–>[Employee Name: ETHAN HARDY  Age: 65  Salary: 8000.0, Employee Name: Nancy Smith  Age: 22  Salary: 12000.0]
2014–>[Employee Name: DEBORAH SPRIGHTLY  Age: 29  Salary: 9000.0]
2015–>[Employee Name: TOM JONES  Age: 45  Salary: 7000.0, Employee Name: Harry Major  Age:25  Salary: 10000.0]

Java 8’s Map.computeIfAbsent() method

As we learnt earlier, a multi-value map stores a collection of values for each key. Lets say we are adding a [key,value] entry to a multi-value map and the key we are adding is not present in the map. This would require for us to check for this probability before insertion and if the key is not present then we will have to instantiate a fresh collection instance as the value for this new key. Only then can we store the value against the key. Also, this check will need to be performed on every insert that we do.

Map.computeIfAbsent() takes away exactly this overhead of writing the multiple line code for such a check by squeezing it into a simple one line code. Map.computeIfAbsent() method is defined as –

default V computeIfAbsent(K key,Function<? super K,? extends V>mappingFunction)
Where,
     – key is the first parameter which is the key of the multi-value map.
     – function is an instance of java.util.function.Function. It computes and returns the value which is to be used when the key is new i.e. does not have a collection instantiated in the case of a multi-value map.

Have a look at the code sample below to understand the difference in code when using Java 7 versus Java 8’s computeIfAbsent() method. The red code is Java 7 way of checking and instantiating before all insertions. The green code is Java 8 way of doing the same thing using Map.computeIfAbsent() method.

Java 8 code to show usage of Map.computeIfAbsent() method

System.out.println("\nJava 7 way of adding a new key(2017) in a multi-value map\n");
List empList2017 = employeeDOJMap.get(2017);
if (empList2017 == null) {
  empList2017 = new ArrayList<>();
}
empList2017.add(new Employee("Tom Newman", 45, 12000.00));
employeeDOJMap.put(2017, empList2017);
employeeDOJMap.forEach((year,empList)-> System.out.println(year+"-->"+empList));
System.out.println("\nUsing Map.computeIfAbsent() to add a new key(2018) in a multi-value map\n");
employeeDOJMap.computeIfAbsent(2018,empList -> new ArrayList<>())
              .add(new Employee("Dick Newman", 35, 10000.00));
employeeDOJMap.forEach((year,empList)-> System.out.println(year+"-->"+empList));


 OUTPUT of the above code


Java 7 way of adding a new key(2017) in a multi-value map
2016–>[Employee Name: Ethan Hardy  Age: 65  Salary: 8000.0, Employee Name: Nancy Smith  Age: 22  Salary: 12000.0]
2017–>[Employee Name: Tom Newman  Age: 45  Salary: 12000.0]
2014–>[Employee Name: Deborah Sprightly  Age: 29  Salary: 9000.0]
2015–>[Employee Name: Tom Jones  Age: 45  Salary: 7000.0, Employee Name: Harry Major  Age:25  Salary: 10000.0]
Using Map.computeIfAbsent() to add a new key(2018) in a multi-value map
2016–>[Employee Name: Ethan Hardy  Age: 65  Salary: 8000.0, Employee Name: Nancy Smith  Age: 22  Salary: 12000.0]
2017–>[Employee Name: Tom Newman  Age: 45  Salary: 12000.0]
2018–>[Employee Name: Dick Newman  Age: 35  Salary: 10000.0]
2014–>[Employee Name: Deborah Sprightly  Age: 29  Salary: 9000.0]
2015–>[Employee Name: Tom Jones  Age: 45  Salary: 7000.0, Employee Name: Harry Major  Age:25  Salary: 10000.0]

Java 8’s new Map.computeIfPresent() method


When working with multi-value maps, there are scenarios when while deleting a [key,value] pair, we might be removing the last/only value in the collection stored as the value for that key. In such cases, after removing the value from the collection, we would want to release the memory occupied by that the empty collection by removing that key from the multi-value map itself. This would require us to check after ‘every’ removal whether the value being removed is the last value for that key, and if so, remove the key from the multi-value map.

Map.computeIfPresent() takes away exactly this overhead of checking after every removal by reducing it to a simple one-line code. Map.computeIfPresent() method is defined as –

default V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V>remappingFunction)
Where,
     – key is the first parameter which is the key of the multi-value map.
     – remappingFunction is an instance of a BiFunction. It computes and returns a value. In case of multi-value maps, the outcome of the key’s collection is decided based the value returned by this function. I.e. whether to keep the collection(if the collection is returned) or delete the collection(if a null value is returned).

To understand the usage of computeIfPresent() method better, let us have a look at the code sample below to understand the difference in code when using Java 7 versus Java 8’s computeIfPresent() method. As we did for the previous method, we will use color coding to differentiate between the Java 7 and Java 8 code. The red code is Java 7 way of checking and removing a key after every removal from the map. The green code is Java 8 way of doing the same thing using Map.computeIfPresent() method.

Java 8 code to show usage of Map.computeIfPresent() method
System.out.println("\nJava 7 way of removing a key(2017) in a multi-value map for which no entry exists\n");
List empListDel = employeeDOJMap.get(2017);
empListDel.removeIf(employee -> employee.getName().equals("Tom Newman"));
if (empListDel.size() == 0) {
  employeeDOJMap.remove(2017);
}
employeeDOJMap.forEach((year, empList)-> System.out.println(year+"-->"+empList));
System.out.println("\nUsing Map.computeIfPresent() to remove a key(2018) for which no entry exists\n");
employeeDOJMap.computeIfPresent(2018, (year, empList) -> empList.removeIf(employee -> employee.getName().equals("Dick Newman")) && empList.size() == 0 ? null : empList);
employeeDOJMap.forEach((year, empList)-> System.out.println(year+"-->"+empList));


 OUTPUT of the above code


Java 7 way of removing a key(2017) in a multi-value map for which no entry exists
2016–>[Employee Name: Ethan Hardy  Age: 65  Salary: 8000.0, Employee Name: Nancy Smith  Age: 22  Salary: 12000.0]
2018–>[Employee Name: Dick Newman  Age: 35  Salary: 10000.0]
2014–>[Employee Name: Deborah Sprightly  Age: 29  Salary: 9000.0]
2015–>[Employee Name: Tom Jones  Age: 45  Salary: 7000.0, Employee Name: Harry Major  Age:25  Salary: 10000.0]
Using Map.computeIfPresent() to remove a key(2018) for which no entry exists
2016–>[Employee Name: Ethan Hardy  Age: 65  Salary: 8000.0, Employee Name: Nancy Smith  Age: 22  Salary: 12000.0]
2014–>[Employee Name: Deborah Sprightly  Age: 29  Salary: 9000.0]
2015–>[Employee Name: Tom Jones  Age: 45  Salary: 7000.0, Employee Name: Harry Major  Age:25  Salary: 10000.0]


Map.getOrDefault() method


Map.getOrDefault() method has been designed for scenarios where the value returned for a given key might be null i.e. the given key is not present in the map. In case of multi-value maps, it gives the programmer a utility to avoid NullPointerException at runtime by instantiating a new collection instance and returning it in case the key is not present and a null-value would otherwise have been returned.

Map.getOrDefault() is defined as follows –

default V getOrDefault(Object key, V defaultValue)
Where,
     – key is the first parameter which is the key of the multi-value map.
     – defaultValue is the value which will be used as default in case the key is not present and a null is returned.

To understand the working of the Map.getOrDefault() method better letter look at the code snippet showing its usage-

Java 8 code to show usage of Map.getOrDefault() method

System.out.println("\nAvoiding a null return when fetching a non-existent key's entry using Map.getOrDefault() method\n");
List<Employee> empList2019 = employeeDOJMap.getOrDefault(2019, new ArrayList<>());
System.out.println("Size of empList 2019 = " + empList2019.size());



No comments:

Post a Comment