Why Java Object equalsTo & hashCode methods come together ?

Object in java inherits 2 important methods from java.lang.Object; one is equalsTo and other one is hasCode  method. They mostly come to the scene together as they have a logical contract or relationship between them if you want to override one of them.

Default Behaviour :

By default equalTo method compares the object references of the two objects and return true or false. So if both are pointing to the same object in the memory then equalsTo return true otherwise false.

boolean equalsTo(Object obj){
    return this == obj;
}


Similarly hasCode method returns a numeric value as per the state of the object. It is a native implementation, so not sure how it is implemented. But if both pointing to the same object in memory they return same hash code value otherwise return different value.

Message m1 = new Message("Hello");
Message m2 = m1;

Then m1.hasCode() and m2.hasCode() will return the same value.

Message m1 = new Message("Hello");
Message m2 = new Message("Hello");

Although m1 & m2 holds the same state Still m1.hasCode() and m2.hasCode() will return different hash code as they are two different objects.

Note :
hasCode() may returns different value at different time during the execution, if the state of the object is modified.

Override Behaviour:

Developers are allowed to override these methods  when required. But while overriding you must ensure that the following rules are not broken.

# If o1.equalsTo(o2) is true then o1.hasCode() must be equals to o2.hasCode() 
# if o1.hasCode() == o2.hasCode() then it is not necessary that o1.equalsTo(o2) is true.

These rules are easy to understand in the context of a HashMap. When you put an entry<key, value> in a HashMap, it uses the hasCode value of the key object to decide which bucket the entry should be stored. And if the bucket has already some entry(s) in it, then it uses the equalsTo method to decide whether to override the value in tan existing entry in the bucket or append the new entry to the bucket.

From above explanation it is found that  hashCode is for grouping objects, so multiple objects  may share the same hasCode. And equalsTo will uniquely identify each object if they are not same.

Suppose you have a Student class as below.

class Student {
    private Integer id; // each student have unique id
    private String name;
 
    public Student(Integer id, String name){
       this.id = id;
       this.name = name;
    }
    // getters & setters
}

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

Case 1#  Using Default Methods

Scenario: 1

map.put(new Student(1, "Foo"),   100);

Integer score = map.get(new Student(1, "Foo")); // return null

In this case, map will have 1 entry in map for Foo(1).  But if we try to get the score of the student Foo(1) with a new student object, we will not find it in map, because they are 2 different objects in memory representing Foo(1) (although both are for the same real world student for Foo(1)).

So if we lost the reference to the key object which is used as key to put an entry to the map, we can not access it from the map.

Scenario: 2

map.put(new Student(1, "Foo"),   100);
map.put(new Student(1, "Foo"),   100);

int keys = map.keySet().size(); // return 2

In this scenario, we will have 2 entries in the map, which is a duplication of the same student Foo trice, as they are different objects in memory (although they represent the same real world student Foo(1)).

So out core objective of using map is failing here.

Case 2# Override equalsTo() only

class Student {
  // assume rest of the code is as defined previously

  @Override
   public boolean equalsTo(Object obj){
       if(this == obj) return true;
       if(obj == null || this.getClass() != obj.getClass()) return false;
       Student other = (Student) obj;
       return this.id == obj.getId();
   }
}

// Test Code
map.put(new Student(1, "Foo"),   100);

Integer score = map.get(new Student(1, "Foo")); // return null

Still in this case we can not access the score for Foo(1) as  now map will store on one bucket based on the hash code value of the initial key object passed to the put method. But while we try to get we are passing a new key object which will have a different hash code value. So map will not able to find with the value as it will try find it in a wrong bucket.

Case 2# Override hasCode() only

class Student {
  // assume rest of the code is as defined previously

  @Override
   public int hasCode(){
       return this.id;
   }
}

// Test Code
map.put(new Student(1, "Foo"),   100);

Integer score = map.get(new Student(1, "Foo")); // return null

Still in this case we can not access the score for Foo(1) as  now map will locate the correct bucket but it will fail to fetch the object from the bucket as the equalsTo method will return false.

Case 3# Override both hasCode & equalsTo

class Student {
  // assume rest of the code is as defined previously

  @Override
   public boolean equalsTo(Object obj){
       if(this == obj) return true;
       if(obj == null || this.getClass() != obj.getClass()) return false;
       Student other = (Student) obj;
       return this.id == obj.getId();
   }

  @Override
   public int hasCode(){
       return this.id;
   }
}

// Test Code
map.put(new Student(1, "Foo"),   100);
map.put(new Student(2, "Bar"),   30);

Integer score1 = map.get(new Student(1, "Foo")); // returns score for Foo(1) -> 100
Integer score2 = map.get(new Student(2, "Bar")); // returns score for  Bar(2) ->   30

So now map will have 2 entries located in 2 different buckets. Foo in bucket(1) and Bar in bucket(2). When we search for Foo, it will get the hash code 1 , so it will search in bucket(1) and there when it will execute equalsTo method over the stored Foo object in bucket it will get true as both of them has same id, so it will return correct entry which has a value 100. Similarly it will do the same for Bar.

So now it should make sense , why equalsTo and hasCode override comes together. For simplicity purpose, I have used the property id only in both the methods, but in practice you can use other properties as well, but always ensure that the properties used in equalsTo must be used in hasCode.

No comments:

Post a Comment