Friday 9 March 2012

Why override equals() and hashCode() methods of HashMap keys in Java

The equals and hashCode methods are declared within the Object class.
Every Java class has Object as a superclass so by default equals and hashCode methods are provided in every java class.The Object class implementation of the equals method returns true only if both the reference variables are referring the same object. I.e. obj1.equals(obj2) returns true only if the objects obj1 and obj2 are referring the same object ,means here simply comparing the memory addresses of the objects not the state of it.The hashCode implementation of the Object class returns an integer value for each objects.But the problem here is that the returning hash code value is not depends on the state of the object.This is typically implemented by converting the internal address of the object into an integer.

In order to implement the hash-based collections we have to override both equals and hashCode methods else it might produce some unexpected and inefficient results.

We have to override the equals method in such a way that is is comparing the states of the objects not the memory addresses.

We have to override the hashCode method in such a way that the hash code value depends on the state of the object i.e. not simply on the memory address.

If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

It is not required that if two objects are unequal according to the equals(java.lang.Object)method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables.

Please go through the following topics to understand more about equal, hashCode methods and how HashMap works in Java.

How and Why to override the equals and hashCode methods in Java

How HashMap works in Java

HashMap works on principle of hashing, we have put () and get () method for storing and retrieving object form HashMap.When we pass both key and value to put() method to store on HashMap, it uses key object hashcode() method to calculate hashcode and they by applying hashing on that hashcode it identifies bucket location for storing value object. Both the key and value will be stored in a LinkedList and address of that linked list stores at a bucket identified by the hashCode.If one more key have the same hash code then that key-value will be stored in next node of linked list.

While retrieving it uses key object's hashCode value to identify the bucket where the value has stored.We know that one bucket may contain more than one key-value pairs, so after finding bucket location , we will call keys.equals() method to identify correct node in linked list and return associated value object for that key in Java HashMap.

See one HashMap storing student-rank data, here we are overriding the equals method of Student class but not the hashCode method.

import java.util.*;

public class Main {

    public static void main(String args[]) {

        Student s1 = new Student(10);

        Student s2 = new Student(10);

        System.out.println("s1.equals(s2)=" + s1.equals(s2));
        System.out.println("s1 hashcode using default hashCode()"

        + s1.hashCode());

        System.out.println("s2 hashcode using default hashCode()"

        + s2.hashCode());

        HashMap<Student, Integer> map = new HashMap<Student, Integer>();

        map.put(s1, 1);

        System.out.println("map.get(s2)=" + map.get(s2));
    }
}

class Student {

    int rollno;

    public Student(int rollno) {

        this.rollno = rollno;
    }

    // @Override
    // public int hashCode() {
    // final int prime = 31;
    // int result = 1;
    // result = prime * result + rollno;
    // return result;
    // }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (rollno != other.rollno)
            return false;
        return true;
    }

}

The output

s1.equals(s2)=true
s1 hashcode using default hashCode()9634993
s2 hashcode using default hashCode()1641745
map.get(s2)=null

You can see that even if both the s1 and s2 have the same data, the default implementation of the hashCode returns different hashcode values for s1 and s2.Hence map.get(s2) is unable to locate the exact bucket where the value 1 is stored ,so get method returns null.

See one more example where the hashCode is overridden but not the equals method

import java.util.*;

public class Main {

    public static void main(String args[]) {

        Student s1 = new Student(10);

        Student s2 = new Student(10);

        System.out.println("s1.equals(s2)=" + s1.equals(s2));
        System.out.println("s1 hashcode using default hashCode()"

        + s1.hashCode());

        System.out.println("s2 hashcode using default hashCode()"

        + s2.hashCode());

        HashMap<Student, Integer> map = new HashMap<Student, Integer>();

        map.put(s1, 1);

        System.out.println("map.get(s2)=" + map.get(s2));
    }
}

class Student {

    int rollno;

    public Student(int rollno) {

        this.rollno = rollno;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + rollno;
        return result;
    }

    // @Override
    // public boolean equals(Object obj) {
    // if (this == obj)
    // return true;
    // if (obj == null)
    // return false;
    // if (getClass() != obj.getClass())
    // return false;
    // Student other = (Student) obj;
    // if (rollno != other.rollno)
    // return false;
    // return true;
    // }

}

The output is

s1.equals(s2)=false
s1 hashcode using default hashCode()41
s2 hashcode using default hashCode()41
map.get(s2)=null
Here map.get(s2) is able to locate exact bucket where the value 1 is stored(both keys s1 & s2 have same hashcode) but not the exact location within the bucket where the value is stored.[Notice that s1.equals(s2)=false]

Finally let us try with overriding both the hashCode and equals method

import java.util.*;

public class Main {

    public static void main(String args[]) {

        Student s1 = new Student(10);

        Student s2 = new Student(10);

        System.out.println("s1.equals(s2)="+s1.equals(s2));
        System.out.println("s1 hashcode using default hashCode()"

        + s1.hashCode());

        System.out.println("s2 hashcode using default hashCode()"

        + s2.hashCode());

        HashMap<Student, Integer> map = new HashMap<Student, Integer>();

        map.put(s1, 1);

        System.out.println("map.get(s2)=" + map.get(s2));
    }
}

class Student {

    int rollno;

    public Student(int rollno) {

        this.rollno = rollno;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + rollno;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (rollno != other.rollno)
            return false;
        return true;
    }

}

The output is

s1.equals(s2)=true
s1 hashcode using default hashCode()41
s2 hashcode using default hashCode()41
map.get(s2)=1

Now we successfully retrieve the value 1 of key s1 from the HashMap using the key s2.That means if you override equals and hashCode methods of key object then using equal key objects you can retrieve value, else you can use only identical key objects to retrieve value.I.e. if you store value using key s1 then you can use only s1 to retrieve value from HashMap.

No comments:

Post a Comment