Skip to content
2 min read

equals() and hashCode() in Java

Getting equals() and hashCode() right is critical for HashMap, HashSet, and any hash-based collection. Getting them wrong causes silent bugs that are extremely hard to debug.


The Contract

Rule Meaning
If a.equals(b) is true a.hashCode() == b.hashCode() MUST be true
If a.hashCode() == b.hashCode() a.equals(b) may or may not be true (hash collisions exist)
Override both or neither Breaking this contract breaks HashMap/HashSet
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    A(["✅ equals returns true"]) -->|MUST| B(["🔑 hashCode MUST be same"])
    C{{"🔑 hashCode is same"}} -->|MAY or MAY NOT| D{{"❓ equals MAY or MAY NOT be true"}}

    style A fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
    style B fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
    style C fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
    style D fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF

Why Both Must Be Overridden Together

What happens if you override only equals()

Java
public class Employee {
    private int id;
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee e)) return false;
        return id == e.id && Objects.equals(name, e.name);
    }
    // hashCode() NOT overridden — uses default (memory address)
}

Employee e1 = new Employee(1, "Vamsi");
Employee e2 = new Employee(1, "Vamsi");

e1.equals(e2);  // true — same id and name

Set<Employee> set = new HashSet<>();
set.add(e1);
set.contains(e2);  // FALSE! — different hashCode (different objects in memory)
set.size();         // 1, but adding e2 would make it 2 (duplicate!)

The bug: e1 and e2 are "equal" but go into different buckets because their hashCodes differ. HashSet thinks they're different objects.


Correct Implementation

Java
public class Employee {
    private int id;
    private String name;
    private String department;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                          // same reference
        if (o == null || getClass() != o.getClass()) return false; // null/type check
        Employee e = (Employee) o;
        return id == e.id
            && Objects.equals(name, e.name)
            && Objects.equals(department, e.department);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, department);  // uses same fields as equals
    }
}

Rules for a correct equals()

Property Meaning
Reflexive x.equals(x) is always true
Symmetric If x.equals(y) then y.equals(x)
Transitive If x.equals(y) and y.equals(z) then x.equals(z)
Consistent Repeated calls return the same result (if objects don't change)
Null x.equals(null) is always false

How HashMap Uses Both

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    subgraph PUT["🔽 map.put - key, value"]
        direction LR
        P1(("1️⃣ hashCode")) -->|"hash % array.length"| P2{{"2️⃣ Go to bucket"}}
        P2 --> P3{"3️⃣ equals - same key?"}
        P3 -->|Yes| P4(["✏️ Replace value"])
        P3 -->|No| P5(["➕ Add as new entry - collision"])
    end

    subgraph GET["🔼 map.get - key"]
        direction LR
        G1(("1️⃣ hashCode")) -->|"bucket index"| G2{{"2️⃣ Walk bucket chain"}}
        G2 --> G3{"3️⃣ equals - matching key?"}
        G3 -->|Yes| G4(["✅ Return value"])
        G3 -->|No| G5(["🔁 Next entry in chain"])
    end

    style PUT fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
    style GET fill:#EFF6FF,stroke:#93C5FD,color:#1E40AF
    style P1 fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
    style P2 fill:#BFDBFE,stroke:#93C5FD,color:#1E40AF
    style P3 fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
    style P4 fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
    style P5 fill:#FCA5A5,stroke:#FCA5A5,color:#1E40AF
    style G1 fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
    style G2 fill:#BFDBFE,stroke:#93C5FD,color:#1E40AF
    style G3 fill:#FEF3C7,stroke:#FCD34D,color:#1E40AF
    style G4 fill:#DBEAFE,stroke:#93C5FD,color:#1E40AF
    style G5 fill:#FCA5A5,stroke:#FCA5A5,color:#1E40AF

If hashCode() is wrong, the key lands in the wrong bucket and get() will never find it — even though equals() would return true.


instanceof vs getClass() in equals

Java
// Using instanceof — allows subclass equality
@Override
public boolean equals(Object o) {
    if (!(o instanceof Employee e)) return false;
    return id == e.id;
}
// new Manager(1) equals new Employee(1) → true

// Using getClass() — strict type equality
@Override
public boolean equals(Object o) {
    if (o == null || getClass() != o.getClass()) return false;
    Employee e = (Employee) o;
    return id == e.id;
}
// new Manager(1) equals new Employee(1) → false

Recommendation: Use getClass() for most cases. Use instanceof only if you explicitly want subclass equality (and understand the symmetry implications).


With Java Records (Java 17+)

Records auto-generate equals() and hashCode() using ALL fields.

Java
public record Employee(int id, String name, String department) {}

Employee e1 = new Employee(1, "Vamsi", "Eng");
Employee e2 = new Employee(1, "Vamsi", "Eng");

e1.equals(e2);    // true
e1.hashCode() == e2.hashCode();  // true

No manual implementation needed.


Using with Lombok

Java
@EqualsAndHashCode
public class Employee {
    private int id;
    private String name;
    private String department;
}

// Or exclude specific fields
@EqualsAndHashCode(exclude = {"department"})
public class Employee {
    private int id;
    private String name;
    private String department;
}

Common Pitfalls

Pitfall Problem
Override equals() but not hashCode() HashMap/HashSet broken
Use mutable fields in hashCode() Object moves to wrong bucket if fields change after insertion
equals() with wrong parameter type equals(Employee e) instead of equals(Object o) — overloads, doesn't override
Include irrelevant fields equals() uses id but hashCode() doesn't — contract broken

Interview Questions

1. What happens if two unequal objects have the same hashCode?

This is called a hash collision. They end up in the same bucket. HashMap handles this by chaining (linked list, or red-black tree for 8+ entries). The equals() method distinguishes them within the bucket. Collisions reduce performance (O(1) → O(n) or O(log n)) but don't break correctness.

2. Can you use a mutable object as a HashMap key?

You can, but you shouldn't. If the object's fields change after insertion, its hashCode() changes, and it ends up in the wrong bucket. get() will never find it — it's a silent memory leak. Always use immutable objects (String, Integer, records) as HashMap keys.

3. What is the best hashCode implementation for performance?

Use Objects.hash(field1, field2, ...) for simplicity. For performance-critical code, manually compute: 31 * hash + field.hashCode() (31 is prime, and 31 * i can be optimized to (i << 5) - i by the JVM). For a single int field, just return the field itself.

4. Why does String make a good HashMap key?

String is immutable (hashCode never changes), caches its hashCode() (computed once), and has a well-distributed hash function. These properties make it the ideal HashMap key. That's also why Java's String.hashCode() is the most called method in the JVM.