Back to Object Class, equals & hashCode
Q4Question 4 of 9

What happens if you override equals() but not hashCode()?

Key Concepts
  • Objects that are equals() will have different hashCode values → violates the contract
  • HashMap.get() looks in the WRONG bucket → returns null even though the key is logically present
  • HashSet.contains() returns false for an equal object
  • The object is effectively lost in any hash-based collection
  • SpotBugs/SonarQube flag this as a critical bug

Definition

HashMap and HashSet break silently. Equal objects land in different buckets because their hash codes differ, so lookups return null and contains returns false.

The consequence:

  • HashMap.get() fails. It computes hashCode of the lookup key, goes to that bucket, and never finds the stored object (which is in a different bucket because its hashCode was computed with Object's default).
  • HashSet.contains() fails. Same reason – wrong bucket.
  • The object is present but unfindable. It is a silent, hard-to-debug data loss.

Filing an account in drawer A (default hashCode) but searching in drawer B (equals-based hashCode). The account is there, but you are looking in the wrong place.

Understanding the Concepts

Demonstration

JAVA
public class Account {
    private final String accountId;

    public Account(String id) { this.accountId = id; }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Account other)) return false;
        return accountId.equals(other.accountId);
    }
    // hashCode NOT overridden → uses Object.hashCode() (memory-based)
}

Map<Account, String> map = new HashMap<>();
Account a = new Account("SAV2001");
map.put(a, "Rajesh");

Account b = new Account("SAV2001");  // equals(a) is true
System.out.println(a.equals(b));     // true
System.out.println(map.get(b));      // null! Different hashCode → different bucket

Set<Account> set = new HashSet<>();
set.add(a);
System.out.println(set.contains(b)); // false! Same reason

The fix

JAVA
@Override
public int hashCode() {
    return accountId.hashCode();  // consistent with equals
}

Now map.get(b) returns "Rajesh" and set.contains(b) returns true.

What should a senior engineer know?

DIAGRAM
- **IDEs generate equals + hashCode together.** IntelliJ: Generate → equals() and hashCode(). Never generate one without the other.
- **Records auto-generate both.** `record Account(String id) {}` – no manual implementation needed.
- **SonarQube rule java:S1206** flags overriding equals without hashCode.

What are common mistakes?

A common mistake is overriding equals() without overriding hashCode(), which breaks HashMap and HashSet behavior.

Wrong approach:

JAVA
package com.bank.model;

public class Account {
    private String accountId;

    // WRONG: overrides equals but not hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Account)) return false;
        return accountId.equals(((Account) o).accountId);
    }
    // Missing hashCode override!
}
JAVA
// This breaks HashMap lookups
Map<Account, String> map = new HashMap<>();
Account a1 = new Account("SAV2001");
map.put(a1, "Rajesh");
Account a2 = new Account("SAV2001");
System.out.println(map.get(a2));  // null! Different hashCode

Correct approach:

JAVA
package com.bank.model;

import java.util.Objects;

public class Account {
    private String accountId;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Account)) return false;
        return accountId.equals(((Account) o).accountId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountId);
    }
}

Now both a1 and a2 produce the same hash code, so HashMap lookups work correctly.

What are the edge cases?

Edge cases can cause unexpected behavior in banking applications. Here are some to watch for.

JAVA
package com.bank.edge;

public class EdgeCaseDemo {
    public static void main(String[] args) {
        // Edge case 1: null handling
        String accountId = null;
        try {
            System.out.println(accountId.length());
        } catch (NullPointerException e) {
            System.out.println("Null account ID");
        }

        // Edge case 2: empty vs null
        String emptyId = "";
        System.out.println(emptyId.isEmpty());  // true
        System.out.println(emptyId == null);     // false

        // Edge case 3: boundary values
        java.math.BigDecimal zero = java.math.BigDecimal.ZERO;
        java.math.BigDecimal negativeBalance = new java.math.BigDecimal("-0.01");
        System.out.println(zero.compareTo(negativeBalance) > 0);  // true
    }
}
TEXT
Null account ID
true
false
true

Always test with null values, empty strings, boundary values, and negative amounts in banking applications.

What are common interview follow-up questions?

Interviewers often follow up with these related questions:

  • How does this concept apply to a high-throughput banking transaction system?
  • What are the performance implications in a production environment?
  • How would you debug issues related to this concept?
  • What tools would you use to monitor this in production?
  • How does this behave differently in Java 8 vs Java 17+?

When answering follow-up questions, always relate your answer back to practical experience. For example, at National Bank of India, Rajesh debugged a production issue related to this concept by using JFR recordings and heap dumps. Providing concrete examples from banking systems shows the interviewer that you understand both the theory and the practice.

Visual Overview

DIAGRAM
  Without hashCode override:
    a = new Account("SAV2001")   hashCode = 12345 (memory-based)
    b = new Account("SAV2001")   hashCode = 67890 (different memory)
    a.equals(b) = true           but hashCode differs!

  HashMap:
    put(a, "Rajesh")  → bucket 12345 % capacity = bucket 5
    get(b)            → bucket 67890 % capacity = bucket 10  ← WRONG bucket
    → returns null (object lost!)

  Fix: override hashCode to use accountId.hashCode()
    → both hash to same bucket → equals finds the match

Key Takeaway

Overriding equals() without hashCode() breaks HashMap and HashSet. Equal objects get different hash codes, land in different buckets, and become unfindable. The object is present but the lookup misses it.

Always override both together using the same fields. Use Objects.hash(), IDE generation, records, or Lombok.

One sentence to remember: override equals without hashCode and your objects vanish from HashMap – always override both together.

---

Share
Use ← → arrow keys to navigate