Skip to content

Part 4.1 — equals() and hashCode() Deep Dive

This is one of the most misunderstood topics in Java interviews.

Many developers memorize:

"Override both equals() and hashCode()."

A senior interviewer will ask:

  • Why?
  • What breaks if you don't?
  • How does HashMap actually use them?

If you cannot answer those questions, you haven't really understood HashMap.


1. What are equals() and hashCode()?

Every Java class inherits these methods from Object.

public class Object {

    public boolean equals(Object obj);

    public int hashCode();
}

Their purposes are different:

  • equals() → Checks logical equality
  • hashCode() → Produces an integer used for hashing

2. Default Behavior

Example:

class Employee {

    int id;

    Employee(int id) {
        this.id = id;
    }
}
Employee e1 = new Employee(1);
Employee e2 = new Employee(1);

System.out.println(e1.equals(e2));

Output:

false

Why?

Because the default implementation compares object references.

Memory:

e1 -----> Employee(id=1)

e2 -----> Employee(id=1)

Different objects.

Different references.

Therefore:

false

3. Reference Equality vs Logical Equality

Reference equality:

Employee e1 = new Employee(1);
Employee e2 = e1;

System.out.println(e1 == e2);

Output:

true

Both variables point to the same object.


Logical equality:

Employee e1 = new Employee(1);
Employee e2 = new Employee(1);

Different objects.

Same business meaning.

If we override equals() correctly:

true

4. == vs equals()

Interview favorite.

String a = new String("Java");
String b = new String("Java");
a == b

Result:

false

Different objects.


a.equals(b)

Result:

true

Same contents.


Operator Compares
== References (or primitive values)
equals() Logical equality (if overridden)

5. Overriding equals()

Example:

class Employee {

    int id;

    Employee(int id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj)
            return true;

        if (obj == null || getClass() != obj.getClass())
            return false;

        Employee other = (Employee) obj;

        return id == other.id;
    }
}

Now:

Employee e1 = new Employee(100);
Employee e2 = new Employee(100);

System.out.println(e1.equals(e2));

Output:

true

6. What is hashCode()?

Example:

Employee e = new Employee(100);

System.out.println(e.hashCode());

Output:

14567382

The actual value is not important.

What matters is the contract.


7. The hashCode() Contract

Rule 1

If two objects are equal,

they must have the same hash code.

Correct:

e1.equals(e2)

↓

true

↓

hashCode must match

Rule 2

If hash codes differ,

objects are definitely not equal.

hash1 != hash2

↓

equals()

need not be checked

This is a major optimization in HashMap.


Rule 3

Same hash code does not imply equality.

Example:

Object A

↓

hash = 12

Object B

↓

hash = 12

Still possible:

equals()

↓

false

This is a collision.


8. Why HashMap Uses hashCode() First

Suppose:

map.get(employee);

HashMap performs:

Step 1

hashCode()

↓

Bucket

↓

Step 2

equals()

only within bucket

Without hashing:

Need to compare

↓

every key

↓

O(n)

With hashing:

Jump directly

↓

one bucket

↓

O(1) average

9. Internal Lookup Flow

Suppose:

Employee e = new Employee(10);

map.get(e);

Internally:

hashCode()

↓

index = hash & (capacity - 1)

↓

Go to bucket

↓

Compare hash

↓

Compare equals()

↓

Return value

Notice:

equals() is not called for every key.

Only keys inside the selected bucket are compared.


10. What Happens If You Override equals() Only?

Very common interview question.

Example:

class Employee {

    int id;

    @Override
    public boolean equals(Object obj) {
        return id == ((Employee)obj).id;
    }
}

No hashCode().

Now:

Employee e1 = new Employee(1);
Employee e2 = new Employee(1);

map.put(e1, "Alice");

System.out.println(map.get(e2));

Expected:

Alice

Actual:

null

Why?

Different default hash codes.

Different buckets.

HashMap never reaches the bucket containing e1.


11. What If You Override hashCode() Only?

Example:

@Override
public int hashCode() {
    return id;
}

But leave equals() unchanged.

Then:

Employee e1 = new Employee(1);
Employee e2 = new Employee(1);

Same hash.

Same bucket.

But:

equals()

↓

false

HashMap thinks they're different keys.

Result:

Duplicate logical entries.


12. Correct Implementation

class Employee {

    int id;

    Employee(int id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj)
            return true;

        if (!(obj instanceof Employee))
            return false;

        Employee other = (Employee)obj;

        return id == other.id;
    }

    @Override
    public int hashCode() {

        return Integer.hashCode(id);
    }
}

Now:

map.put(new Employee(1), "Alice");

map.get(new Employee(1));

Works correctly.


13. Mutable Keys (Critical Production Bug)

Example:

class Employee {

    int id;
}
Employee e = new Employee();

e.id = 100;

map.put(e, "Alice");

Later:

e.id = 200;

Now:

map.get(e);

May return:

null

Why?

The object's hash code changed after insertion.

It now belongs in a different bucket than where it was stored.

Rule: Never mutate fields that participate in equals() or hashCode() while the object is being used as a key.


14. Bad hashCode() Implementation

Example:

@Override
public int hashCode() {
    return 1;
}

Everything goes into:

Bucket 1

Result:

Bucket 1

↓

Node

↓

Node

↓

Node

↓

Node

Performance:

O(n)

Instead of:

O(1)

15. Good hashCode()

Example:

@Override
public int hashCode() {
    return Objects.hash(id, name);
}

Produces a much better distribution.

Fewer collisions.

Better performance.


16. Objects.hash()

Example:

@Override
public int hashCode() {
    return Objects.hash(id, department, salary);
}

Readable.

Correct.

Suitable for most business classes.

Interview note: Objects.hash() creates an array internally, so in performance-critical classes you may prefer a manually computed hash (or IDE-generated implementation). The difference is usually negligible for typical enterprise applications.


17. Java Records

Java records automatically generate:

equals()

hashCode()

toString()

Example:

record Employee(int id, String name) {}

This is one reason records make excellent immutable map keys.


18. Common Interview Questions

Q1. Why must equal objects have equal hash codes?

Because HashMap first locates the bucket using hashCode(). If equal objects produce different hash codes, they end up in different buckets and lookups fail.


Q2. Can unequal objects have the same hash code?

Yes.

This is called a hash collision.


Q3. Does the same hash code mean objects are equal?

No.

equals() must still be used to determine logical equality.


Q4. Which method is called first?

hashCode()

↓

equals()

Always.


Q5. Can a bad hash function affect performance?

Yes.

Poor distribution increases collisions and degrades lookup performance.


Because changing a key's state after insertion can change its hash code or equality, making the entry difficult or impossible to retrieve.


equals() Contract

A correct implementation must satisfy:

  1. Reflexive: x.equals(x) is always true.
  2. Symmetric: If x.equals(y) is true, then y.equals(x) must also be true.
  3. Transitive: If x.equals(y) and y.equals(z) are true, then x.equals(z) must also be true.
  4. Consistent: Repeated calls give the same result if the objects don't change.
  5. Non-null: x.equals(null) must always return false.

Violating these rules can cause subtle bugs not only in HashMap, but also in HashSet, TreeSet (when combined with inconsistent comparison logic), and other collections.


Senior Interview Scenario

Question:

Map<Employee, String> map = new HashMap<>();

Employee e1 = new Employee(1);

map.put(e1, "Alice");

Employee e2 = new Employee(1);

System.out.println(map.get(e2));

Follow-up:

It prints null. Why?

Expected reasoning:

  • HashMap computes the bucket using hashCode().
  • If Employee doesn't override hashCode(), e1 and e2 have different identity-based hash codes.
  • e2 is searched in a different bucket.
  • equals() is never invoked against e1.
  • Therefore, map.get(e2) returns null.

Key Takeaways

  • equals() defines logical equality.
  • hashCode() determines the bucket in hash-based collections.
  • HashMap always uses hashCode() before equals().
  • Equal objects must have equal hash codes.
  • Collisions are normal and resolved within a bucket using equals().
  • Never use mutable objects as keys unless the fields affecting equality and hashing remain unchanged.
  • Records and immutable classes are excellent choices for HashMap keys.

Next: Part 4.2 — HashMap Internal Implementation Deep Dive

We'll go beneath the API and study the actual insertion and resize algorithms, including:

  • The putVal() method
  • Bitwise hash spreading (h ^ (h >>> 16))
  • Resize and rehash mechanics
  • Why resize is expensive
  • Java 7 vs Java 8 resize differences
  • Infinite loop issue in Java 7 under concurrent modification
  • Memory layout and bucket redistribution
  • Source-code-level interview discussion