Jun 2, 2010

How to Implement Hashcode() and Equals(): Dos and Don'ts

The hashcode() and equals() method have always intrigued me a lot! I had never wanted to dive in depth into this! However, I thought I should dive deep into the bet practice related to the implementation of equals() and hascode() for domain objects.

Before we dive deep into this let me tell you the problems that you might confront if you do not override hashcode() and equals() in your persistent domain objects

Consequences of Not Overriding the Equals() and Hashcode()

  1. When you try to load the same persistent object from 2 different sessions, then you might have unexpected results. Let us say that we have the following code where in the domain object Sample does not have the overridden equals() and hashcode()
  2. int i=2;//2 is the existing id of the Sample object in the db
    Sample s1 = firstSession.load(Sample.class, i);
    Sample s2 = secondSession.load(Sample.class, i);
    assert(s1.equals(s2));
    
    In the above case, we would expect that the assert statement would return true! But this would never be the case since the s1.equals(s2) would just make a == comparison and would return false. For a simple Sample class given below:
    public class Sample {
    private int id;
    private int rollNo;
    
    public int getId() {
    return id;
    }
    
    public void setId(int id) {
    this.id = id;
    }
    
    public int getRollNo() {
    return rollNo;
    }
    
    public void setRollNo(int rollNo) {
    this.rollNo = rollNo;
    }
    }
    
    The following code would return false if the hashcode() and equals() are not overridden
    Sample s2 = new Sample();
    s2.setRollNo(2);
    s2.setId(1);
    Sample s3 = new Sample();
    s3.setRollNo(2);
    s3.setId(1);
    System.out.println("am here:"+s2.equals(s3));
    
  3. If you are planning to use the domain object as a composite id, it is imperative that you override both the methods. Else the equals() of the Object class would be used where in the == would be used and it would never suffice!

ID Based Equals() and Hashcode() Implementation: Hidden Dangers

One of the very common implementation of the equals() and hashcode() methods that I come across is as given below: It uses the id property for the implementation
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Sample)){
return false;
}
Sample other = (Sample)obj;
if (!(other.getId() == this.id)) return false;
return true;
}

@Override
public int hashCode() {
return this.id;
}
But how far is the above code reliable? Is this the right way of implementing the equals() and hashcode()? In case of ORM solutions like Hibernate, where the ORM would not generate the id till it saves the domain object to the db, the above implementation proves to be hazardous. Here is a very nice article that tells the problems associated with the implementation given above!

  1. First and foremost, putting the unsaved domain objects with equals() and hashcode() (in ORM like Hibernate) described above into sets would produce unexpected results.
  2. Let us have a look at the below code:
    Sample s = new Sample();
    s.setRollNo(2);
    Sample s1 = new Sample();
    s.setRollNo(2);
    Set<Sample> newSet = new HashSet<Sample>();
    newSet.add(s);
    newSet.add(s1);
    System.out.println(newSet.contains(s));// returns true
    System.out.println(newSet.contains(s1));// returns true
    System.out.println("size:" + newSet.size()); // returns 1: unexpectedly
    // due to improper
    // implementation of
    // equals() and hashcode()
    
    In the above code, the id of both s and s1 would be 0 if int or null if the type is Integer. As a result, the second s1 when put in the set overrides the first s object and hence when tested for contains the hashcode() method returns 0 for id and hence both return true. But, the set size is just 1!
  3. Secondly, the collections would produce unexpected results before and after saving them to the db. Lets see this issue in detail.Consider the following code:
  4. HashSet newSet = new HashSet();
    Sample s4 = new Sample();
    newSet .add(s4);
    session.save(s4);
    assert(newSet .contains(s4)); // returns false since the id before and after save changes.
    

Here the id of the Sample object before saving is 0 and after saving is different and is determined by the ORM. As a result the assert returns false in contrary to what is expected!

Best Alternative Implementation

Amidst such issues, the best option would be to implement equals() and hashcode() using a combination of business keys! In an IDE like Eclipse. it is going to be easy. Just right click, point to source --> Generate equals() and hashcode() and you have the option as shown below to choose the business keys that you want to be a part of the equals and hashcode() checks. With all, this proves to be the best and the most reliable option to me! However, do keep me updated with your suggestions too!

Best Practices
  1. Ensure that your customized implementation abides by the contract dictated by sun.
  2. Ensure that you are making use of a combination of immutable business keys of the domain object for the implementation.
  3. Do have a look at the hibernate forum discussion before you go ahead withe the implementation.
  4. Yet another article that is worth checking is here. Have a look and then go ahead with your customizations further!