Friday, November 16, 2012

Overriding equals() and hashCode() on the right way.


Comparison Java objects

We know that there are two basic ways to compare Java objects: use the “==” operation and the equals() method.
Assume that we have a class Customer as below:

class Customer {
     private short year;
     private String name;

     public Customer(short year, String name) {
          super();
          this.year = year;
          this.name = name;
     }

     public short getYear() {
          return year;
     }

     public String getName() {
          return name;
     }
    
     // Add more code here
}
The “==” operation
The result of this operation is true when two variables refer to exactly on object. If we write and run the code as below on the main() method, we will see that the results are false and true:

     public static void main(String[] args) {
          Customer c1 = new Customer((short) 1985, "Java85");
          Customer c2 = new Customer((short) 1985, "Java85");
          Customer c3 = c2;
          System.out.println(c1 == c2);// Line 1
          System.out.println(c3 == c2);// Line 2
     }

Here c1 and c2 variables refer two different objects although the attributes are the same values. Otherwise, c2 and c2 variables refer to the second object created, so c3 == c3 returns true.
The equals() method
The equals() method is used when we want to compare different objects based on their attributes. For example in the real life, if we see two cubes having the same size and color on each sides, we could say that they equal.
Each object, when it is created, will inheritance the equals() method of java.lang.Object. On the default implementation, the equals() method just compare two objects by the “==” operation. Therefore, if we change Line 1 and Line 2 on the main() method to become:

          System.out.println(c1.equals(c2));// Line 1
          System.out.println(c3.equals(c2));// Line 2

the results are also false and true. In this case, we want that c1.equals(c2) returns true because their attributes are the same values. To do it, we need to override the equals() method. We should compare attributes that we think that they are important and necessary to compare two objects. For our example, we will compare year and name attributes of Customer class. Here is one way to override the equals() method:

     @Override
     public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (obj instanceof Customer) {
              Customer c = (Customer) obj;
              if (c.getName().equals(name) && c.getYear() == year)
                   return true;
          }
          return false;
     }

We add this code after the commend line // Add more code here on the Customer code. After that, if you run again the main() method, you will have true and true on your output.
Until here, it is enough if you just want to compare values of objects. You also could use with the contains() method of java.util.List.

          List<Customer> list = new ArrayList<Customer>();
          list.add(c1);
          System.out.println(list.contains(c1));
          System.out.println(list.contains(c2));

Execute the code, the result should be true and true . It is fine.
However, it is not enough to work with java.util.Set and java.util.Map. We could se this code:

          Map<Customer, Integer> map = new HashMap<Customer, Integer>();
          map.put(c1, 2012);
          System.out.println(map.get(c1));
          System.out.println(map.get(c2));

Running the code above, we want to see 2012 and 2012 on the result view, but the real result are 2012 and null. We look more on the example for using java.util.Set:

          Set<Customer> set = new HashSet<Customer>();
          set.add(c1);
          set.add(c2);
          System.out.println(set.size());

The same with the code for java.util.Map, this code you may want to see the size of that set is 1 because c1 equals to c2, but the real size is 2.
What is going wrong here? It is because java.util.Map and java.util.Set use hash code values of objects to compare them. To get exactly what we want, we must override the hashCode() method for Customer class.

An example for the hashCode() method

We could add the overriding code for the hastCode() method for Customer class as below:

     @Override
     public int hashCode() {
          return name.hashCode() + year;
     }

Then we run the code examples for java.util.Map and java.util.Set, we will see the results now are 2012 and 2012 for the java.util.Map example; and 1 for the java.util.Set example.
However, it is not a right way to override the hashCode() method though it is simple and easy to understand. We should override it on the way mentioned on the Effective Java ver.2 book of Joshua Bloch.

The way to override hashCode()

Follow that recommendation, we will override it by these steps:

  1. Declare a constant, for instance, int hashCode = 85;
  2. For each significant (important and necessary) attribute, we do:
        a. Compute the hashCode fc for the field f:

  • .If f is a boolean: fc = f ? 1: 0
  • .If f is a byte, short, char or int: fc = (int) f
  • .If f is a long: fc = (int) (f ^ (f>>>32))
  • .If f is a float: fc = Float.floatToIntBits(f)
  • .If f is a double: fc = Double.doubleToLongBits(f). And then calculate hashCode for fc as a long type.
  • .If f is an array, calculate for each element as a separate field.
  • .If f is an object, fc = f.hashCode()
        b. Combine fc with hashCode: hashCode = 31 * hashCode + c;

     3.   Return hashCode.

Some notes:

  • You could ignore insignificant fields.
  • The first value hastCode = 85 is arbitrary.
  • The number 31 on the step 2b could change, but it is better if it is a prime number. The value 31 is a good choice for performance when 31 * i = (i << 5) – i.
Now we come back to Customer class. We could override the hashCode() in the effective way:

     @Override
     public int hashCode() {
          int hashCode = 85;
          hashCode = 31 * hashCode + year;// year is an int
          hashCode = 31 * hashCode + name.hashCode();// name is an object (String)
          return hashCode;
     }

Now it is fine.

Reference

Effective Java ver.2 - Joshua Bloch

No comments:

Post a Comment