Back to Object Class, equals & hashCode
Q3Question 3 of 9

What is the contract between equals() and hashCode()?

Key Concepts
  • If two objects are equal by equals(), they MUST have the same hashCode()
  • If two objects have the same hashCode(), they are NOT necessarily equal (hash collision)
  • Breaking this contract breaks HashMap and HashSet – objects become unfindable
  • equals() contract: reflexive, symmetric, transitive, consistent, null returns false
  • hashCode() contract: consistent during the object's lifetime, same for equal objects

Definition

If a.equals(b) is true, then a.hashCode() must equal b.hashCode(). Breaking this contract makes objects unfindable in HashMap and HashSet.

The contract:

DIAGRAM
- **equals() true → hashCode() must be same.** Required for HashMap/HashSet.
- **hashCode() same → equals() may or may not be true.** Hash collisions are allowed.
- **If you override equals(), you must override hashCode().** Breaking this rule is the #1 cause of "HashMap lost my object" bugs.

The bank files accounts in a cabinet by the first letter of the account holder's name (hashCode). Two people with the same name hash to the same drawer. If equals says two accounts are the same but they hash to different drawers, the bank can never find the second one.

Understanding the Concepts

The contract in code

JAVA
public class Account {
    private final String accountId;

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

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

Account a = new Account("SAV2001");
Account b = new Account("SAV2001");

a.equals(b);       // true
a.hashCode() == b.hashCode();  // MUST be true (contract)

What happens if you break it?

JAVA
// BROKEN: equals uses accountId, but hashCode uses default (Object)
public class BrokenAccount {
    private String accountId;

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

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

BrokenAccount b = new BrokenAccount("SAV2001");
System.out.println(map.get(b));  // null! b's hashCode differs from a's
// a.equals(b) is true, but they land in different buckets

How to implement correctly

JAVA
// Using Objects.hash for multi-field hashCode
@Override
public int hashCode() {
    return Objects.hash(accountId, accountType);
}

// Or for records: auto-generated from all components
public record Account(String accountId, AccountType type) {}

What should a senior engineer know?

  • HashMap uses hashCode to find the bucket, then equals to match within the bucket. If hashCode is wrong, the bucket is wrong, and equals is never called.
  • Use @EqualsAndHashCode from Lombok or records to avoid manual implementation errors.
  • Mutable fields in hashCode are dangerous. If a field used in hashCode changes after the object is in a HashMap, the object becomes unfindable.
  • JPA entities: use business key (accountId), not generated ID, for equals/hashCode because the generated ID is null before persist.

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
  Contract:
    a.equals(b) == true  →  a.hashCode() == b.hashCode()  (REQUIRED)
    a.hashCode() == b.hashCode()  →  a.equals(b) may or may not be true  (collision OK)

  HashMap:
    put(key, value) → bucket = hashCode % capacity → store in bucket
    get(key) → bucket = hashCode % capacity → scan bucket with equals

  Broken hashCode → wrong bucket → equals never checked → object lost

Key Takeaway

The equals-hashCode contract says: if two objects are equal by equals(), they must have the same hashCode(). Breaking this rule makes objects unfindable in HashMap and HashSet because the lookup goes to the wrong bucket.

Always override both together. Use the same fields in both methods. Use Objects.hash() for convenience, or let records and Lombok generate them.

One sentence to remember: equal objects must have equal hash codes – break this and HashMap loses your data.

---

Share
Use ← → arrow keys to navigate