Back to Object Class, equals & hashCode
Q6Question 6 of 9

How do you make a custom object a valid HashMap key?

Key Concepts
  • Override equals() and hashCode() consistently using the same fields
  • Make the key immutable (or at least the fields used in equals/hashCode) to prevent hash corruption
  • Use the business key (accountId), not the generated ID (which is null before persist)
  • Records make ideal HashMap keys: auto-generated equals, hashCode, immutable
  • Understanding this concept is essential for Java interviews at senior level

Definition

Override equals() and hashCode() using the same fields, and make the key immutable. If the hash-relevant fields change after insertion, the object becomes unfindable.

Three requirements for a HashMap key:

  1. Override equals() – defines when two keys are the same.
  2. Override hashCode() – consistent with equals for correct bucket placement.
  3. Immutable key fields – if a field used in hashCode changes, the bucket changes but the object stays in the old bucket.

A bank filing system indexed by account number. The account number (hash key) must be permanent. If someone changes the account number after filing, the file is lost in the old drawer.

Understanding the Concepts

Correct implementation

JAVA
public final class AccountKey {
    private final String accountId;    // immutable
    private final String branchCode;   // immutable

    public AccountKey(String accountId, String branchCode) {
        this.accountId = Objects.requireNonNull(accountId);
        this.branchCode = Objects.requireNonNull(branchCode);
    }

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

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

// Usage
Map<AccountKey, BigDecimal> balances = new HashMap<>();
balances.put(new AccountKey("SAV2001", "MUM01"), new BigDecimal("500000"));
balances.get(new AccountKey("SAV2001", "MUM01"));  // 500000 ✓

Even better: use a record

JAVA
public record AccountKey(String accountId, String branchCode) {}
// equals, hashCode, toString auto-generated, fields are final

What happens with mutable keys?

JAVA
public class MutableKey {
    private String id;  // mutable!
    // equals and hashCode use id
}

MutableKey key = new MutableKey("SAV2001");
map.put(key, "Rajesh");
key.setId("CUR3001");  // hashCode changes, but the object is still in the old bucket!
map.get(key);  // null! Wrong bucket, object lost
map.get(new MutableKey("SAV2001"));  // also null! Old hash bucket, but key now says CUR3001

What should a senior engineer know?

  • String, Integer, Long, BigDecimal are all immutable and safe as HashMap keys.
  • JPA entities should NOT be used as HashMap keys by their generated @Id (null before persist). Use the business key.
  • ConcurrentHashMap has the same key requirements plus thread safety.

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
  Valid HashMap key requires:
    1. equals() overridden     → defines equality
    2. hashCode() overridden   → consistent with equals
    3. Key fields immutable    → hash stays stable after insertion

  Best: record AccountKey(String id, String branch) {}
  → auto equals, hashCode, immutable fields

Key Takeaway

A valid HashMap key needs equals() and hashCode() overridden consistently, and the fields used in those methods must be immutable. If a key's hash-relevant field changes after insertion, the object becomes unfindable.

Records are ideal HashMap keys because they auto-generate equals, hashCode, and have immutable fields.

One sentence to remember: override equals + hashCode, keep key fields immutable – or use a record.

---

Share
Use ← → arrow keys to navigate