In Java, the contract between equals() and hashCode() is fundamental for any developer. If you override one, you must override the other. Breaking this rule leads to bugs where objects are "lost" inside collections like HashMap or HashSet.

In this guide, we will look at a correct implementation using a Book class, examine a broken implementation in an Employee class, and review key class design pitfalls.

Illustration of a bookshelf and ID badges with matching barcodes representing the equals and hashCode contract
Real-World Analogy: Library Sorting Boxes

Imagine a library sorting room where books are sorted into physical boxes labeled by their ISBN:

  • The Shelf Box (hashCode()): When a book arrives, you look up its ISBN to know which shelf box to put it in. This is the **hashCode**.
  • Checking the Book (equals()): If you want to check if the book is already in that box, you do a detailed page comparison. This is the **equals** check.

If two books have the exact same ISBN (i.e. they are equal), they MUST go into the same box (return the same hash code). If they go into different boxes because their hash codes don't match, you will never find them!

1. The Correct Implementation: Book.java

Let's look at the Book class, which demonstrates a correct, clean contract. A book is uniquely identified by its ISBN:

public class Book {
    private int ISBN;
    private String author, title;
    private int pageCount;

    @Override
    public int hashCode() {
        return ISBN; // Matches the equals comparison field
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Book)) {
            return false;
        }
        Book other = (Book) obj;
        return this.ISBN == other.ISBN;
    }
}

Since equals() compares ISBN and hashCode() returns ISBN, equal books will always produce identical hash codes. This is a perfect implementation.

2. Pitfalls in the Broken Implementation: Employee.java

Now let's examine the Employee class from our codebase. It contains three major bugs:

public class Employee {
    private int empId;
    private String fName, lName;
    private int yearStarted;

    public int hashcode() { // Pitfall 1: Lowercase 'c'
        return this.yearStarted + empId;
    }

    @Override
    public boolean equals(Object obj) {
        return this.yearStarted == ((Employee) obj).yearStarted; // Pitfall 2 & 3
    }
}
Pitfall 1: Method Overloading instead of Overriding (hashcode vs hashCode)

Notice the method is named hashcode() with a lowercase c. Because of this typo, Java does not override the standard Object.hashCode(). It treats it as a brand new method. HashMap will still call the default memory address-based hash code, causing equal employees to be lost in different buckets.

Solution: Always use the @Override annotation. The compiler will trigger an error if the method name is misspelled.

Pitfall 2: Unsafe Casting without Type Checks

The equals() method immediately casts obj to Employee. If you call employee.equals("John"), the program will crash with a ClassCastException. Always verify with instanceof first.

Pitfall 3: Weak Equality Logic

The equals() method only checks yearStarted. This means any two employees who started working in the same year are considered identical! An employee should be compared using their unique ID (like empId).

Conclusion

When designing your objects:

  • Always override hashCode() whenever you override equals().
  • Double-check capitalization: it's hashCode() with a capital C.
  • Guard your casts in `equals()` using `instanceof` checks.