Search code examples
javaequalshashcodeeffective-java

Effective java item 9: overriding hashcode example


I was reading Effective Java Item 9 and decided to run the example code by myself. But it works slightly different depending on how I insert a new object that I don't understand what exactly is going on inside. The PhoneNumber class looks:

public class PhoneNumber {

private final short areaCode;
private final short prefix;
private final short lineNumber;

public PhoneNumber(int areaCode, int prefix, int lineNumber) {
    this.areaCode = (short)areaCode;
    this.prefix = (short) prefix;
    this.lineNumber = (short)lineNumber;
}

@Override public boolean equals(Object o) {
    if(o == this) return true;
    if(!(o instanceof PhoneNumber)) return false;
    PhoneNumber pn = (PhoneNumber)o;
    return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
}
}

Then according to the book and as is when I tried,

    public static void main(String[] args) {

         HashMap<PhoneNumber, String> phoneBook = new HashMap<PhoneNumber, String>();
         phoneBook.put(new PhoneNumber(707,867,5309), "Jenny");
         System.out.println(phoneBook.get(new PhoneNumber(707,867,5309)));
    }

This prints "null" and it's explained in the book because HashMap has an optimization that caches the hash code associated with each entry and doesn't check for object equality if the hash codes don't match. It makes sense to me. But when I do this:

    public static void main(String[] args) {

         PhoneNumber p1 = new PhoneNumber(707,867,5309);
         phoneBook.put(p1, "Jenny");
         System.out.println(phoneBook.get(new PhoneNumber(707,867,5309)));
    }

Now it returns "Jenny". Can you explain why it didn't fail in the second case?


Solution

  • The experienced behaviour might depend on the Java version and vendor that was used to run the application, because since the general contract of Object.hashcode() is violated, the result is implementation dependent.

    A possible explanation (taking one possible implementation of HashMap):

    The HashMap class in its internal implementation puts objects (keys) in different buckets based on their hashcode. When you query an element or you check if a key is contained in the map, first the proper bucket is looked for based on the hashcode of the queried key. Inside the bucket objects are checked in a sequencial way, and inside a bucket only the equals() method is used to compare elements.

    So if you do not override Object.hashcode() it will be indeterministic if 2 different objects produce default hashcodes which may or may not determine the same bucket. If by any chance they "point" to the same bucket, you will still be able to find the key if the equals() method says they are equal. If by any chance they "point" to 2 different buckets, you will not find the key even if equals() method says they are equal.

    hashcode() must be overriden to be consistent with your overridden equals() method. Only in this case it is guaranteed the proper, expected and consistent working of HashMap.

    Read the javadoc of Object.hashcode() for the contract that you must not violate. The main point is that if equals() returns true for another object, the hashcode() method must return the same value for both of these objects.