Back to Special Maps & Collections
Q3Question 3 of 5

What happens with a mutable StringBuilder key in HashMap?

Key Concepts
  • StringBuilder does not override equals() or hashCode(), so it uses Object's identity-based implementations.
  • Two StringBuilder objects with the same text are treated as different keys because equals() compares references, not content.
  • Mutating a StringBuilder after inserting it as a key does not change its identity hash, but a content-based hash would become stale.
  • HashMap keys must be effectively immutable: if the hashCode changes after insertion, the entry is stranded in the wrong bucket.
  • String, Integer, Long, and BigDecimal are good HashMap keys because they are immutable and have proper equals/hashCode.
  • In banking code, always use String account identifiers or BigDecimal amounts as keys, never mutable types.
  • If you must use a mutable object as a key, call toString() or convert it to an immutable representation first.

Definition

Using a mutable StringBuilder as a HashMap key fails because StringBuilder uses identity-based equals/hashCode and because mutating the key after insertion strands the entry in the wrong bucket.
  • No equals/hashCode override – StringBuilder inherits Object.equals() (reference comparison) and Object.hashCode() (identity hash), so two builders with the same text are different keys
  • Mutable state – even if StringBuilder overrode hashCode based on content, mutating the builder after insertion would change the hash, making the entry unreachable
  • Double failure – a StringBuilder key fails at lookup time (identity equals) and fails after mutation (stale bucket position)
  • Rule – HashMap keys must be immutable with value-based equals and hashCode
  • Safe alternatives – String, Integer, Long, BigDecimal, and custom immutable classes with proper equals/hashCode

Imagine Rajesh at National Bank of India labels a filing drawer with a pencil tag that says "SAV2001". The filing clerk memorises which shelf it is on based on the original tag. Later Rajesh erases the tag and writes "CUR3001". When someone searches for the drawer labelled CUR3001, the clerk looks on the wrong shelf because the drawer was filed under the original label. With an immutable printed label (a String), the label never changes and the drawer is always found.

Understanding the Concepts

What happens with a mutable StringBuilder key in HashMap?

StringBuilder inherits equals() and hashCode() from Object. This means equals() checks reference identity (==), and hashCode() returns the identity hash code. Two StringBuilder objects with the same text are considered different keys.

JAVA
package com.bank.demo;

import java.util.HashMap;
import java.util.Map;

public class StringBuilderKeyDemo {
    public static void main(String[] args) {
        Map<StringBuilder, String> map = new HashMap<>();
        StringBuilder key = new StringBuilder("SAV2001");
        map.put(key, "Rajesh");

        // Lookup with the same object works
        System.out.println("Same object: " + map.get(key));

        // Lookup with a new StringBuilder with same text fails
        StringBuilder lookup = new StringBuilder("SAV2001");
        System.out.println("New object same text: " + map.get(lookup));

        // Mutation: change the key after insertion
        key.append("-CLOSED");
        System.out.println("After mutation, same ref: " + map.get(key));
        System.out.println("Map size: " + map.size());
    }
}
TEXT
Same object: Rajesh
New object same text: null
After mutation, same ref: Rajesh
Map size: 1

The lookup with a new StringBuilder returns null because key.equals(lookup) is false (identity comparison). The mutated key still works with the same reference only because Object.hashCode() is identity-based and does not change with content. However, this behaviour is fragile and misleading.

Why do mutable keys break HashMap?

HashMap relies on a contract: hashCode() must be consistent with equals(), and both must remain stable for the lifetime of the key inside the map. If a key's hashCode changes after insertion, the entry is stranded in the wrong bucket. A get() call computes the new hashCode, looks in a different bucket, and finds nothing.

With StringBuilder, the problem is twofold. First, equals() uses identity, so content-based lookup is impossible. Second, if StringBuilder did override hashCode based on content, mutating it would strand the entry.

JAVA
package com.bank.demo;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class MutableKeyStrandedDemo {

    // A mutable key class that DOES override hashCode based on content
    static class MutableAccount {
        String id;
        MutableAccount(String id) { this.id = id; }

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

        @Override
        public boolean equals(Object o) {
            return o instanceof MutableAccount m && Objects.equals(id, m.id);
        }
    }

    public static void main(String[] args) {
        Map<MutableAccount, String> map = new HashMap<>();
        MutableAccount key = new MutableAccount("SAV2001");
        map.put(key, "Rajesh");

        System.out.println("Before mutation: " + map.get(key));

        key.id = "CUR3001"; // mutate the key after insertion
        System.out.println("After mutation (same ref): " + map.get(key));
        System.out.println("Lookup with old id: " + map.get(new MutableAccount("SAV2001")));
        System.out.println("Map size: " + map.size());
    }
}
TEXT
Before mutation: Rajesh
After mutation (same ref): null
Lookup with old id: null
Map size: 1

The entry exists in the map (size is 1), but no lookup can find it. It is stranded in the bucket computed from the original hashCode "SAV2001", while all lookups now compute the hashCode for "CUR3001".

How do you fix this?

The fix is simple: always use immutable objects as HashMap keys. Convert mutable objects to their immutable representation before using them as keys.

JAVA
package com.bank.demo;

import java.util.HashMap;
import java.util.Map;

public class ImmutableKeyFixDemo {
    public static void main(String[] args) {
        // WRONG: StringBuilder as key
        Map<StringBuilder, String> bad = new HashMap<>();
        bad.put(new StringBuilder("SAV2001"), "Rajesh");
        System.out.println("StringBuilder lookup: " + bad.get(new StringBuilder("SAV2001")));

        // CORRECT: String as key
        Map<String, String> good = new HashMap<>();
        good.put("SAV2001", "Rajesh");
        System.out.println("String lookup: " + good.get("SAV2001"));

        // CORRECT: Convert StringBuilder to String first
        StringBuilder builder = new StringBuilder("CUR3001");
        good.put(builder.toString(), "Priya");
        System.out.println("Converted lookup: " + good.get("CUR3001"));
    }
}
TEXT
StringBuilder lookup: null
String lookup: Rajesh
Converted lookup: Priya

What are the rules for good HashMap keys?

A well-behaved HashMap key must satisfy these rules.

  • Immutable. The fields used in equals() and hashCode() must never change after the object is used as a key.
  • Consistent equals and hashCode. If a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true.
  • Value-based equality. Two objects with the same logical content should be equal.

Good key types in banking code:

| Type | Immutable | Value-based equals/hashCode | Safe as key |
|---|---|---|---|
| String | Yes | Yes | Yes |
| Integer, Long | Yes | Yes | Yes |
| BigDecimal | Yes | Yes (with caveats on scale) | Yes |
| StringBuilder | No | No (identity-based) | No |
| Date (java.util) | No (mutable) | Yes | Dangerous |
| LocalDate | Yes | Yes | Yes |

What are common mistakes?

Mistake 1: Using StringBuilder directly as a key

JAVA
// WRONG
Map<StringBuilder, String> map = new HashMap<>();
map.put(new StringBuilder("SAV2001"), "Rajesh");
System.out.println(map.get(new StringBuilder("SAV2001"))); // null
JAVA
// CORRECT
Map<String, String> map = new HashMap<>();
map.put("SAV2001", "Rajesh");
System.out.println(map.get("SAV2001")); // Rajesh

Mistake 2: Using java.util.Date as a key and mutating it

JAVA
import java.util.*;

// WRONG
Map<Date, String> map = new HashMap<>();
Date key = new Date();
map.put(key, "Transaction");
key.setTime(0L); // mutate after insertion
System.out.println(map.get(key)); // null! Stranded entry.
JAVA
// CORRECT: Use immutable LocalDate
import java.time.LocalDate;
import java.util.*;

Map<LocalDate, String> map = new HashMap<>();
map.put(LocalDate.now(), "Transaction");
System.out.println(map.get(LocalDate.now())); // Transaction

What should a senior engineer know?

  • Effective Java Item 11: Always override hashCode when you override equals. Conversely, if a class does not override both, it should not be used as a HashMap key.
  • BigDecimal caveat. new BigDecimal("1.0").equals(new BigDecimal("1.00")) returns false because scale differs. If you use BigDecimal as a map key, use stripTrailingZeros() or consider using the compareTo-based TreeMap.
  • Records (Java 16+) automatically generate equals and hashCode based on all components, making them excellent HashMap keys as long as their components are immutable.
  • Defensive copies. When receiving a mutable object to use as a key, always make a defensive copy: map.put(new String(mutableBuilder.toString()), value).

What are common interview follow-up questions?

  • "Why can't you use StringBuilder as a HashMap key?" : It does not override equals/hashCode, so it uses identity comparison. Two builders with the same text are different keys.
  • "What happens if you mutate a key after inserting it?" : The entry is stranded in the old bucket. Lookups fail even with the same reference.
  • "What makes a good HashMap key?" : Immutability, consistent equals/hashCode, value-based equality. String, Integer, and BigDecimal are ideal.
  • "Can you use a custom class as a key?" : Yes, if it properly overrides equals and hashCode and is effectively immutable.

Visual Overview

DIAGRAM
  Why mutable keys break HashMap
  ==============================

  put(key="SAV2001", value="Rajesh"):
  ┌───────────────────────────────────────┐
  │ hashCode("SAV2001") = 1234            │
  │ bucket index = 1234 & (capacity - 1)  │
  │ Entry stored in bucket 4              │
  └───────────────────────────────────────┘

  Mutate key: key.id = "CUR3001"
  ┌───────────────────────────────────────┐
  │ Entry is STILL in bucket 4            │
  │ (HashMap does not know key changed)   │
  └───────────────────────────────────────┘

  get(key) after mutation:
  ┌───────────────────────────────────────┐
  │ hashCode("CUR3001") = 5678            │
  │ bucket index = 5678 & (capacity - 1)  │
  │ Looks in bucket 10 -> EMPTY -> null!  │
  └───────────────────────────────────────┘

  Entry is stranded in bucket 4.
  No lookup can reach it.
  Map size = 1, but effectively lost.

Key Takeaway

Using a mutable object like StringBuilder as a HashMap key is a double failure. First, StringBuilder does not override equals or hashCode, so it uses identity comparison and two builders with the same text are treated as different keys. Second, mutating any key after insertion changes its logical state while the entry remains in the bucket computed from the original hash, making it unreachable by any lookup.

The fix is straightforward: always use immutable types as HashMap keys. String, Integer, Long, BigDecimal, and LocalDate all have proper value-based equals and hashCode and cannot be mutated after creation. If you must work with a StringBuilder, convert it to a String with toString() before using it as a key.

At National Bank of India, Pankaj established a coding rule that every map key in the banking codebase must be an immutable type with value-based equality. Account identifiers are always String (SAV2001, CUR3001), amounts are always BigDecimal, and dates are always LocalDate. This eliminated an entire class of silent lookup failures that could cause a balance query to return null in a system that handles real money.

Remember one sentence: HashMap keys must be immutable with value-based equals and hashCode, so use String instead of StringBuilder and LocalDate instead of Date.

---

Share
Use ← → arrow keys to navigate