Monday 28 January 2019

Using Custom Classes as Keys in HashMaps

Using Custom Classes as Keys in HashMaps


Today, we are going to discuss what we need to keep in mind when we want to use our custom class as a key in HashMap.
Here, we are considering an Employee class as a key having Id, Name, DateOfBirth, Salary as the properties.

Problem Statement 1

Let's consider the implementation below of the Employee class:
public class Employee {       
    private long id;   
    private String name;   
    private Date dateOfBirth;    
    private BigDecimal salary;   
    //Getter and Setters
    // toString
}

Let's consider using the above class as a HashMap key. For the value element for the HashMap, we are choosing a string for this example:
public static void main(String[] args) {    
    HashMap<Employee,String> employeeMap = new HashMap<Employee,String>();  
    Employee employee1 = new Employee();    
    employee1.setId(1);    
    employee1.setName("Sachin");    
    employee1.setDateOfBirth(new Date(1987,2,1));    
    employee1.setSalary(new BigDecimal(100000));    
    employeeMap.put(employee1,"India");    
    // Some Business logic    
    // In the second Operation I am updating the same employee with the newly initailized Employee Object
    Employee employee2 = new Employee();    
    employee2.setId(1);    
    employee2.setName("Sachin");    
    employee2.setDateOfBirth(new Date(1987,2,1));    
    employee2.setSalary(new BigDecimal(100000));  
    // Here we wanted to update the same Employee to Japan
    employeeMap.put(employee2,"Japan");    
    System.out.println(employeeMap); 
    // Output of this will be 2 as below
    /* 
        {Employee{id=1, name='Sachin', dateOfBirth=Tue Mar 01 00:00:00 IST 3887, salary=100000}=Japan, 
        Employee{id=1, name='Sachin', dateOfBirth=Tue Mar 01 00:00:00 IST 3887, salary=100000}=India}
    */
}

Solution for Problem Statement 1

The problem was that the hashcode and the Equals method qwew generated from the Object class. employee1 and employee2's generated hashcode will be different, and two Employee objects will be present in the HashMap.
By making the Employee class implement the Object class's equals and hashcode methods, both the employee1 and employee2 hashcode and equals methods will return the same thing.
The HashMap will use the hashcode and equals method to identify the bucket where the object is present and equals to check that the properties values are same. It's going to retrieve the correct value.
Equals and hashcode implementation in the Employee class:
public class Employee {    
    private long id;    
    private String name;    
    private Date dateOfBirth;    
    private BigDecimal salary;    
    //Getter and Setters    
    // to String    
    @Override    
    public boolean equals(Object o) {        
        if (this == o) return true;        
        if (o == null || getClass() != o.getClass()) return false;        
        Employee employee = (Employee) o;        
        if (id != employee.id) return false;        
        if (name != null ? !name.equals(employee.name) : employee.name != null) return false;        
        if (dateOfBirth != null ? !dateOfBirth.equals(employee.dateOfBirth) : employee.dateOfBirth != null) return false;        
        return salary != null ? salary.equals(employee.salary) : employee.salary == null;    
    }    
    @Override    
    public int hashCode() {        
        int result = (int) (id ^ (id >>> 32));        
        result = 31 * result + (name != null ? name.hashCode() : 0);        
        result = 31 * result + (dateOfBirth != null ? dateOfBirth.hashCode() : 0);
        result = 31 * result + (salary != null ? salary.hashCode() : 0);
        return result;    
    }    
}

Problem Statement 2

Employee is a mutable object. That will create problems with a HashMap. Let's look at the code below:
public static void main(String[] args) {    
    HashMap<Employee,String> employeeMap = new HashMap<Employee,String>();  
    Employee employee1 = new Employee();   
    employee1.setId(1);   
    employee1.setName("Sachin");   
    employee1.setDateOfBirth(new Date(1987,2,1));   
    employee1.setSalary(new BigDecimal(100000));
    // Step 1
    employeeMap.put(employee1,"India");   
    for (Map.Entry<Employee, String> employeeStringEntry : employeeMap.entrySet()) {
        System.out.println(employeeStringEntry.getKey().hashCode());   
    }
    // Step 2
    // Mutating the Employee Object
    employee1.setName("Rahul");    
    for (Map.Entry<Employee, String> employeeStringEntry : employeeMap.entrySet()) {
        System.out.println(employeeStringEntry.getKey().hashCode());    
    }
    // The HashMap key is mutated and in the wrong bucket for that hashcode. 
    // Step 3
    System.out.println(employeeMap.get(employee1));    
    // Returns null    
    Employee employee2 = new Employee();   
    employee2.setId(1);   
    employee2.setName("Sachin");   
    employee2.setDateOfBirth(new Date(1987,2,1));   
    employee2.setSalary(new BigDecimal(100000));
    System.out.println(employeeMap.get(employee2)); 
    // Returns null
}

Once the Employee Object is mutated, the hashcode of that object is going to change. Now, if we try to retrieve it in step 3 (with the different hashcode), it will go to a different bucket and not be able to get the value. Now, the object placed in the HashMap is lost forever.

Solution for Problem Statement 2

Make the Key object immutable so that mutation of the key will not affect the key element of the HashMap, so the HashMap will be consistent. Below is the implementation of an immutable Employee class via with Builder pattern.
public final class Employee {
    private final long id;
    private final String name;
    private final Date dateOfBirth;
    private final BigDecimal salary;
    public Employee(EmployeeBuilder employeeBuilder) {
        this.id = employeeBuilder.id;
        this.name = employeeBuilder.name;
        this.dateOfBirth = employeeBuilder.dateOfBirth;
        this.salary = employeeBuilder.salary;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        if (id != employee.id) return false;
        if (name != null ? !name.equals(employee.name) : employee.name != null) return false;
        if (dateOfBirth != null ? !dateOfBirth.equals(employee.dateOfBirth) : employee.dateOfBirth != null)
            return false;
        return salary != null ? salary.equals(employee.salary) : employee.salary == null;
    }
    @Override
    public int hashCode() {
        int result = (int) (id ^ (id >>> 32));
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (dateOfBirth != null ? dateOfBirth.hashCode() : 0);
        result = 31 * result + (salary != null ? salary.hashCode() : 0);
        return result;
    }
    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", dateOfBirth=" + dateOfBirth +
                ", salary=" + salary +
                '}';
    }
    public long getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public Date getDateOfBirth() {
        return (Date) dateOfBirth.clone();
    }
    public BigDecimal getSalary() {
        return salary;
    }
    public static final class EmployeeBuilder {
        private long id;
        private String name;
        private Date dateOfBirth;
        private BigDecimal salary;
        private EmployeeBuilder() {
        }
        public static EmployeeBuilder anEmployee() {
            return new EmployeeBuilder();
        }
        public static EmployeeBuilder anEmployee(Employee employee) {
            return anEmployee().withId(employee.getId()).withName(employee.getName()).withDateOfBirth(employee.getDateOfBirth()).withSalary(employee.getSalary());
        }
        public EmployeeBuilder withId(long id) {
            this.id = id;
            return this;
        }
        public EmployeeBuilder withName(String name) {
            this.name = name;
            return this;
        }
        public EmployeeBuilder withDateOfBirth(Date dateOfBirth) {
            this.dateOfBirth = dateOfBirth;
            return this;
        }
        public EmployeeBuilder withSalary(BigDecimal salary) {
            this.salary = salary;
            return this;
        }
        public Employee build() {
            return new Employee(this);
        }
    }
}

Now update the code of the test to see if the HashMap works consistently:
public static void main(String[] args) {
    HashMap<Employee,String> employeeMap = new HashMap<Employee,String>();
    Employee employee1 = Employee.EmployeeBuilder.anEmployee().withId(1)
                                .withName("Sachin")
                                .withDateOfBirth(new Date(1987, 2, 1))
                                .withSalary(new BigDecimal(100000))
                                .build();
    employeeMap.put(employee1,"India");
    for (Map.Entry<Employee, String> employeeStringEntry : employeeMap.entrySet()) {
        System.out.println(employeeStringEntry.getKey().hashCode());
    }
    Employee immutableUpdatedEmployee1 = Employee.EmployeeBuilder.anEmployee(employee1).withName("Rahul").build();
    for (Map.Entry<Employee, String> employeeStringEntry : employeeMap.entrySet()) {
        System.out.println(employeeStringEntry.getKey().hashCode());
    }
    System.out.println(employeeMap.get(immutableUpdatedEmployee1));
    // Returns null
    Employee employee2 = Employee.EmployeeBuilder.anEmployee().withId(1)
            .withName("Sachin")
            .withDateOfBirth(new Date(1987, 2, 1))
            .withSalary(new BigDecimal(100000))
            .build();
    System.out.println(employee2.hashCode());
    System.out.println(employeeMap.get(employee2));
    // Now this works fine and it shall return  the correct object from the HashMap
}

No comments:

Post a Comment

40 Latest Interview Questions and Answers on Spring, Spring MVC, and Spring Boot

  40 Latest Interview Questions and Answers on Spring, Spring MVC, and Spring Boot 1. What is Tight Coupling? When a class (ClassA) is depen...