Mid-level 9 min · March 06, 2026

Wrapper Keys Cause IdentityHashMap Serialization Loss

ContainsKey false, duplicated integer keys, identical strings map twice—all from IdentityHashMap's ==.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • IdentityHashMap uses == (reference equality) not equals() for key comparison
  • Internally uses open addressing with a flat Object[] — no Entry nodes
  • Hash function is System.identityHashCode(key), stable across GC moves
  • Faster than HashMap for small maps due to no equals() calls and no per-entry allocation
  • Production risk: autoboxed Integer keys outside -128..127 produce multiple entries silently
  • Never pass to code expecting standard Map semantics — violates Map contract intentionally
✦ Definition~90s read
What is IdentityHashMap in Java?

IdentityHashMap is a specialized Map implementation in Java that uses reference equality (==) instead of Object.equals() for key comparisons. This is a deliberate violation of the general Map contract, which mandates equals()-based behavior.

Imagine you have two photocopies of the same driver's license.

It exists to solve a specific problem: when you need to track object identities, not values — for example, in serialization frameworks, proxy caches, or debugging tools where two distinct instances with identical fields must be treated as separate entries. Unlike HashMap, which relies on hashCode() and equals(), IdentityHashMap uses System.identityHashCode() and reference checks, making it ideal for scenarios like maintaining a graph of object references or implementing a shallow copy tracker.

Internally, IdentityHashMap uses a linear-probing hash table with a flat array structure — no separate buckets or linked lists. This design trades collision resolution simplicity for cache locality and predictable iteration order (insertion order, unlike HashMap).

However, this same structure causes serialization loss when wrapper keys (e.g., Integer, String) are used: deserialization creates new objects, breaking reference equality. If you serialize an IdentityHashMap keyed by Integer.valueOf(42), the deserialized map will fail to retrieve the value because the new Integer instance has a different identity.

This is not a bug — it's a fundamental consequence of the contract. Use IdentityHashMap only when you control key lifecycle (e.g., interned strings, unique proxies) or when serialization is not required. For thread safety, wrap it with Collections.synchronizedMap(); it does not support concurrent access natively.

In production, IdentityHashMap shines in frameworks like Spring (for singleton bean registries), Hibernate (first-level cache tracking), or serialization libraries (e.g., Jackson's ObjectMapper uses it internally to detect circular references). It outperforms HashMap for identity-based lookups due to simpler hashing and no equals() overhead, but iteration can be slower for large maps due to linear probing.

Compare it to WeakHashMap (weak references for garbage collection) and EnumMap (ultra-fast, enum-keyed maps with array backing). Use IdentityHashMap when you need reference semantics and can tolerate its quirks; avoid it for general-purpose mapping or when keys are serialized across JVM boundaries.

Plain-English First

Imagine you have two photocopies of the same driver's license. A normal librarian checks if they look identical — same name, same photo — and treats them as the same card. An IdentityHashMap librarian ignores all that. She only cares whether you're handing her the exact same physical card. Two identical-looking cards are still two different cards to her. That's reference equality: it's not about what something is, it's about which specific object in memory you're pointing to.

Most Java developers reach for HashMap without thinking twice, and 95% of the time that's the right call. But there's a narrow, important category of problems where HashMap's equality semantics actively work against you — and IdentityHashMap was built precisely for those moments. Understanding it isn't just trivia; it reveals how Java's object model actually works at a low level.

The core problem IdentityHashMap solves is this: HashMap uses equals() and hashCode() to compare keys. If two different object instances happen to be equal by those contracts, HashMap considers them the same key. Sometimes that's exactly what you don't want. Think of serialization frameworks tracking which object instances have already been visited, or proxy systems that need to store metadata keyed to a specific object — not to any logically equivalent copy of it. Using == instead of equals() to compare keys is the entire point of this class.

By the end of this article you'll understand how IdentityHashMap stores data without calling equals() or hashCode(), why its internal array layout is unusual, when it's the right tool for production code, and what traps will bite you if you confuse it with a general-purpose map. You'll also have a solid answer ready for the interviewer who asks about it — because it comes up more than people expect.

Why IdentityHashMap Breaks Serialization with Wrapper Keys

IdentityHashMap is a Map implementation that uses reference equality (==) instead of Object.equals() for key comparisons. This means two distinct Integer objects both holding the value 5 are treated as different keys. The core mechanic: hash codes come from System.identityHashCode(), not the key's hashCode() override. This bypasses the contract expected by most Java collections.

When you serialize an IdentityHashMap, the keys are written as ordinary objects. On deserialization, the JVM creates new instances of wrapper types like Integer, Long, or String. Because these new objects have different memory addresses than the originals, IdentityHashMap's reference-equality check fails — every key becomes a miss. The map appears empty or loses entries, even though the serialized data is correct.

Use IdentityHashMap only when you explicitly need reference-based identity, such as tracking object graphs in a serializer, maintaining per-instance metadata, or implementing a debug-level interning cache. Never use it with wrapper keys (Integer, Long, etc.) unless you control serialization and ensure identity is preserved — which standard Java serialization does not. In production, this mistake silently corrupts state.

Serialization Surprise
IdentityHashMap with Integer keys will lose all entries after deserialization because new Integer instances have different references.
Production Insight
A caching layer used IdentityHashMap<Integer, Session> to avoid equals() overhead. After a rolling restart, all sessions were orphaned — the map returned null for every lookup.
The symptom: 100% cache miss rate post-restart, logged as 'session not found' even though serialized data existed.
Rule: Never use IdentityHashMap with autoboxed or wrapper keys unless you guarantee identity across serialization boundaries.
Key Takeaway
IdentityHashMap uses reference equality (==), not equals().
Wrapper keys (Integer, Long, String) lose identity on deserialization.
Use IdentityHashMap only for identity-sensitive cases like object graph tracking, never for value-based keys.
IdentityHashMap Serialization Loss with Wrapper Keys THECODEFORGE.IO IdentityHashMap Serialization Loss with Wrapper Keys How wrapper keys break serialization due to reference equality IdentityHashMap Uses Reference Equality Compares keys by ==, not equals() Wrapper Keys Create New Objects Deserialization produces distinct instances Serialization Fails to Find Key New wrapper doesn't match original reference Data Loss on Deserialization Entries become unreachable and lost Use IdentityHashMap Only for Identity When object identity is the key semantics ⚠ Never use wrapper keys (e.g., Integer, String) in IdentityHashMap Use primitive keys or ensure identity is preserved across serialization THECODEFORGE.IO
thecodeforge.io
IdentityHashMap Serialization Loss with Wrapper Keys
Identityhashmap Java

How IdentityHashMap Works Internally — Not Your Typical Hash Table

Most hash maps use separate chaining or tree bins (HashMap switched to tree bins in Java 8 for large buckets). IdentityHashMap does neither. It uses open addressing with linear probing on a flat array — and that array stores keys and values interleaved at consecutive indices. Key goes in slot 2i, value in slot 2i+1. This is a single Object[] array, not an array of Entry nodes. There are zero node objects. That means less garbage and better cache locality.

The hash function is also intentionally different. Instead of calling key.hashCode(), it uses System.identityHashCode(key) — the JVM-assigned identity hash, which is based on the object's memory address (or a stored snapshot of it before GC moves the object). This means two objects that are .equals() but are different instances will almost certainly land in different buckets.

The table size is always a power of two, and the default capacity is 32 key-value pairs (so the backing array starts at length 64). The load factor is fixed at 2/3 — you can't change it. When the map exceeds 2/3 full, it resizes by doubling. Understanding this layout matters for performance: IdentityHashMap can be faster than HashMap for small maps with frequent put/get because there's no object allocation per entry and no equals() call overhead.

IdentityHashMapInternals.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.IdentityHashMap;
import java.util.Map;

public class IdentityHashMapInternals {

    public static void main(String[] args) {

        // Two String objects with IDENTICAL content but different references.
        // 'new String(...)' deliberately bypasses the string pool so we get
        // two physically separate objects in the heap.
        String keyA = new String("session");
        String keyB = new String("session");

        System.out.println("--- Equality checks ---");
        System.out.println("keyA.equals(keyB) : " + keyA.equals(keyB));  // true  — same content
        System.out.println("keyA == keyB       : " + (keyA == keyB));    // false — different objects
        System.out.println("identityHashCode A : " + System.identityHashCode(keyA));
        System.out.println("identityHashCode B : " + System.identityHashCode(keyB));

        // ------- IdentityHashMap behaviour -------
        IdentityHashMap<String, String> identityMap = new IdentityHashMap<>();

        identityMap.put(keyA, "value-for-A");
        identityMap.put(keyB, "value-for-B");  // keyB != keyA by ==, so this is a SECOND entry

        // Two entries exist even though the keys look identical!
        System.out.println("\n--- IdentityHashMap ---");
        System.out.println("Size (expect 2) : " + identityMap.size());
        System.out.println("Get with keyA   : " + identityMap.get(keyA));  // value-for-A
        System.out.println("Get with keyB   : " + identityMap.get(keyB));  // value-for-B

        // ------- HashMap behaviour for contrast -------
        Map<String, String> hashMap = new java.util.HashMap<>();

        hashMap.put(keyA, "value-for-A");
        hashMap.put(keyB, "value-for-B");  // keyB.equals(keyA) == true, so this OVERWRITES

        System.out.println("\n--- HashMap (for comparison) ---");
        System.out.println("Size (expect 1) : " + hashMap.size());  // only 1 entry survives
        System.out.println("Get with keyA   : " + hashMap.get(keyA)); // value-for-B (overwritten)
        System.out.println("Get with keyB   : " + hashMap.get(keyB)); // value-for-B
    }
}
Output
--- Equality checks ---
keyA.equals(keyB) : true
keyA == keyB : false
identityHashCode A : 1829164700
identityHashCode B : 2018699554
--- IdentityHashMap ---
Size (expect 2) : 2
Get with keyA : value-for-A
Get with keyB : value-for-B
--- HashMap (for comparison) ---
Size (expect 1) : 1
Get with keyA : value-for-B
Get with keyB : value-for-B
Why identityHashCode doesn't change after GC moves the object
The JVM computes the identity hash code on first request and caches it in the object's mark word. Even when the garbage collector moves the object to a new memory address during compaction, the cached hash stays the same. So System.identityHashCode() is stable for the object's lifetime — IdentityHashMap doesn't break under GC pressure.
Production Insight
Linear probing degrades when load factor exceeds 2/3 — the default is fixed and you can't tune it.
If you hit many hash collisions, probe chains become long and get/put times climb.
Rule: keep map sizes under ~500 entries; for larger, consider a different reference-equality structure or a custom open-addressing map with configurable load factor.
Key Takeaway
IdentityHashMap's flat array and lack of Entry objects make it memory-efficient and cache-friendly for small maps.
The trade-off: no tree-binning means degradation under collisions.
Use it for maps you keep small (dozens to low hundreds), not for millions of entries.

When IdentityHashMap Is the Right Tool — Real Production Scenarios

The most common production use case is object graph traversal where you need to track visited nodes. If your nodes override equals() to compare by content (which many domain objects do), a regular HashMap will incorrectly say 'already visited' for two distinct objects that happen to be equal by value. IdentityHashMap solves this cleanly because it truly tracks which specific heap object you've seen.

Java's own serialization mechanism (ObjectOutputStream) uses exactly this pattern internally — it maintains an IdentityHashMap to remember which object references have already been written so it can emit back-references instead of re-serializing the whole object.

Another strong use case is proxy and AOP frameworks. When you wrap objects with dynamic proxies, you often need to store metadata per proxy instance — not per logical equality of the proxied object. A cache keyed on the proxy reference must use reference equality, otherwise two different proxy wrappers around an equivalent target would incorrectly share the same metadata.

A third scenario: weak-reference caches and canonicalization tables. When you're building a system that interns objects (ensuring only one canonical instance exists), you need to look up by reference at certain stages to avoid infinite loops during the canonicalization itself.

ObjectGraphVisitor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.util.IdentityHashMap;
import java.util.ArrayList;
import java.util.List;

/**
 * Demonstrates IdentityHashMap as a 'visited' tracker during
 * recursive object graph traversal. This is the pattern Java's
 * own serialization engine uses internally.
 */
public class ObjectGraphVisitor {

    // A simple node that deliberately overrides equals() by content.
    // This is the key: two nodes with the same label are 'equal' by value
    // but may be two physically separate objects in a cyclic graph.
    static class GraphNode {
        String label;
        List<GraphNode> neighbours = new ArrayList<>();

        GraphNode(String label) { this.label = label; }

        // equals() and hashCode() are based on label content — NOT reference
        @Override public boolean equals(Object other) {
            if (!(other instanceof GraphNode)) return false;
            return this.label.equals(((GraphNode) other).label);
        }
        @Override public int hashCode() { return label.hashCode(); }
        @Override public String toString() { return "Node(" + label + ")"; }
    }

    // IdentityHashMap as a visited-set. Value is irrelevant; we use Boolean.TRUE
    // as a lightweight placeholder — the key IS the visited tracking mechanism.
    private final IdentityHashMap<GraphNode, Boolean> visited = new IdentityHashMap<>();

    public void traverse(GraphNode startNode) {
        // containsKey uses == internally — correct for cycle detection
        if (visited.containsKey(startNode)) {
            System.out.println("Already visited (by reference): " + startNode);
            return;
        }

        visited.put(startNode, Boolean.TRUE);  // mark this exact object as seen
        System.out.println("Visiting: " + startNode);

        for (GraphNode neighbour : startNode.neighbours) {
            traverse(neighbour);  // recurse into neighbours
        }
    }

    public static void main(String[] args) {
        GraphNode nodeA = new GraphNode("auth-service");
        GraphNode nodeB = new GraphNode("user-service");
        GraphNode nodeC = new GraphNode("auth-service");  // SAME label as nodeA, different object!

        // Build a graph: A -> B -> C -> A (cycle back to A, not C)
        nodeA.neighbours.add(nodeB);
        nodeB.neighbours.add(nodeC);
        nodeC.neighbours.add(nodeA);  // points back to the ORIGINAL nodeA instance

        System.out.println("nodeA.equals(nodeC) = " + nodeA.equals(nodeC)); // true — same label!
        System.out.println("nodeA == nodeC      = " + (nodeA == nodeC));    // false — different objects
        System.out.println();

        // If we used a HashSet<GraphNode> or HashMap<GraphNode,...> for 'visited',
        // visiting nodeC would INCORRECTLY appear as 'already visited' because
        // nodeC.equals(nodeA) == true. IdentityHashMap prevents that false positive.
        new ObjectGraphVisitor().traverse(nodeA);
    }
}
Output
nodeA.equals(nodeC) = true
nodeA == nodeC = false
Visiting: Node(auth-service)
Visiting: Node(user-service)
Visiting: Node(auth-service)
Already visited (by reference): Node(auth-service)
Use Collections.newSetFromMap() to get an IdentityHashSet
Java has no built-in IdentityHashSet, but you can create one in one line: Set<MyType> identitySet = Collections.newSetFromMap(new IdentityHashMap<>()). It delegates set semantics to the map's key-equality rules, giving you a fully working reference-equality Set without rolling your own.
Production Insight
ObjectOutputStream uses IdentityHashMap internally to detect cyclic references.
If you implement custom serialization, use the same pattern: IdentityHashMap for visited set, not HashMap.
Mistaking them leads to StackOverflowError in deep object graphs — a silent crash caller code rarely catches.
Key Takeaway
IdentityHashMap is your tool when you need to track object identity, not logical equality.
Graph traversal, proxy metadata, and serialization back-references are the classic wins.
Don't use it for general key-value storage — that's what HashMap is for.

Performance Profile, Gotchas, and the Intentional Contract Violation

Here's the elephant in the room: IdentityHashMap intentionally violates the Map contract. The Map interface javadoc states that a map should use equals() to compare keys. IdentityHashMap explicitly documents that it violates this. This is fine — but it means you must never pass an IdentityHashMap to code that expects standard Map semantics. If a library method accepts Map<K,V> and internally calls containsKey with a freshly constructed equal key, it will fail silently and mysteriously.

On the performance side, IdentityHashMap wins on three fronts for small-to-medium maps: no equals() call on get/put (just a single == comparison), no per-entry Node object allocation (entries live directly in the flat array), and better CPU cache utilisation from the interleaved array layout. For large maps with many hash collisions, linear probing can degrade — but in practice IdentityHashMap is used for maps in the dozens-to-hundreds range, not millions.

One subtle gotcha: autoboxing. If you use primitive-wrapper types like Integer as keys, be careful. Integer.valueOf(127) returns a cached instance, so two calls with the value 127 return == the same object. But Integer.valueOf(1000) creates new instances each time. Your map's behaviour changes based on the integer value — a deeply non-obvious bug.

AutoboxingTrap.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.IdentityHashMap;

/**
 * Demonstrates the autoboxing trap in IdentityHashMap.
 * This is one of the most surprising bugs you can hit in production
 * when using IdentityHashMap with Integer keys.
 */
public class AutoboxingTrap {

    public static void main(String[] args) {

        IdentityHashMap<Integer, String> sessionMap = new IdentityHashMap<>();

        // Integer.valueOf() caches values from -128 to 127.
        // Both of these calls return THE EXACT SAME cached Integer object.
        Integer sessionIdSmall1 = Integer.valueOf(42);   // returns cached instance
        Integer sessionIdSmall2 = Integer.valueOf(42);   // returns the SAME cached instance

        System.out.println("--- Small Integer (within cache range) ---");
        System.out.println("sessionIdSmall1 == sessionIdSmall2 : " + (sessionIdSmall1 == sessionIdSmall2)); // true!

        sessionMap.put(sessionIdSmall1, "user-alice");
        sessionMap.put(sessionIdSmall2, "user-bob");   // same reference! OVERWRITES, size stays 1

        System.out.println("Map size (expect 1 due to cache): " + sessionMap.size()); // 1
        System.out.println("Value: " + sessionMap.get(Integer.valueOf(42)));           // user-bob

        sessionMap.clear();

        // Integer.valueOf(1000) is OUTSIDE the cache range.
        // Each call creates a brand new Integer object on the heap.
        Integer sessionIdLarge1 = Integer.valueOf(1000); // new object
        Integer sessionIdLarge2 = Integer.valueOf(1000); // another new object

        System.out.println("\n--- Large Integer (outside cache range) ---");
        System.out.println("sessionIdLarge1 == sessionIdLarge2 : " + (sessionIdLarge1 == sessionIdLarge2)); // false!

        sessionMap.put(sessionIdLarge1, "user-charlie");
        sessionMap.put(sessionIdLarge2, "user-diana");  // different reference — TWO entries!

        System.out.println("Map size (expect 2, surprise!): " + sessionMap.size()); // 2

        // Trying to retrieve with a THIRD Integer(1000) won't find anything
        // because it's yet another new reference, unknown to the map.
        Integer lookupAttempt = Integer.valueOf(1000);
        System.out.println("Lookup with new Integer(1000): " + sessionMap.get(lookupAttempt)); // null!

        // FIX: Always hold onto the exact reference you put in, and use it to retrieve.
        System.out.println("Lookup with original ref     : " + sessionMap.get(sessionIdLarge1)); // user-charlie
    }
}
Output
--- Small Integer (within cache range) ---
sessionIdSmall1 == sessionIdSmall2 : true
Map size (expect 1 due to cache): 1
Value: user-bob
--- Large Integer (outside cache range) ---
sessionIdLarge1 == sessionIdLarge2 : false
Map size (expect 2, surprise!): 2
Lookup with new Integer(1000): null
Lookup with original ref : user-charlie
Watch Out: Never use String literals or interned Strings as IdentityHashMap keys
String literals are interned by the JVM — 'String s = "hello"' always returns the same pooled object. This means two literal 'hello' keys always share the same reference, which makes them behave like a normal map. That might look correct but it's accidental and fragile. The moment someone passes a non-interned String (e.g. from user input or a database), the reference equality breaks down and lookups return null. Always use new String(...) in tests to validate your IdentityHashMap behaves the way you intend.
Production Insight
Never pass IdentityHashMap to a method that expects Map — it's not a drop-in replacement.
The method may create keys internally using equals() logic, and the identity map won't find them.
Result: silent corruption, missing entries, or infinite loops. Type the variable as IdentityHashMap, not Map.
Key Takeaway
IdentityHashMap's contract violation is deliberate but dangerous.
Keep it local, don't leak it through Map-typed APIs.
Autoboxing trap: Integer keys outside cached range create multiple distinct entries — hold the reference.

Thread Safety, Iteration, and Memory Considerations

IdentityHashMap is not thread-safe — just like HashMap. But there's an extra subtlety: because it uses open addressing with linear probing, concurrent modification can corrupt the internal array in ways that silent data loss even more easily than chained maps. A put during an iteration can cause the iterator to skip entries or throw ConcurrentModificationException.

Iteration order is unpredictable — same as HashMap. The iterator returned by entrySet() etc. is fail-fast. For thread-safe access, wrap with Collections.synchronizedMap(), but note that iteration still requires external synchronization. There's no concurrent identity map in the JDK — you'd need to roll your own using striped locks or a custom structure.

Memory-wise, the flat Object[] array grows in powers of two. For a map with N entries, the array length is at least 2 * nextPowerOfTwo(N / 0.666). That can waste space if N is just above a threshold. For example, 33 entries cause a resize to capacity 64, giving an array of 128 slots. If you're memory-constrained, consider initial capacity carefully via the constructor new IdentityHashMap<>(expectedSize). It allocates an array sized to hold expectedSize entries at 2/3 load without immediate resize.

IdentityHashMapThreadSafety.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Collections;

public class IdentityHashMapThreadSafety {

    public static void main(String[] args) throws InterruptedException {
        // NOT thread-safe — concurrent put may cause infinite loop or lost updates
        IdentityHashMap<String, String> unsafe = new IdentityHashMap<>();
        
        // Thread 1: puts 1000 entries
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // Use new Strings to avoid interning — distinct references
                unsafe.put(new String("key" + i), "value-" + i);
            }
        });
        
        // Thread 2: iterates concurrently
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // May throw ConcurrentModificationException or see inconsistent state
                for (Map.Entry<String, String> e : unsafe.entrySet()) {
                    // simulate read
                    String v = e.getValue();
                }
                Thread.yield();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Final size (expected 1000, but may be less due to lost updates): " + unsafe.size());
        
        // Thread-safe wrapper: external synchronization still required for iteration
        Map<String, String> safe = Collections.synchronizedMap(new IdentityHashMap<>());
        
        // Example of correct iteration with explicit sync
        synchronized (safe) {
            for (Map.Entry<String, String> e : safe.entrySet()) {
                // safe iteration
            }
        }
    }
}
Output
Final size (expected 1000, but may be less due to lost updates): 987
(May also see ConcurrentModificationException or thread hangs)
No concurrent alternative in JDK
If you need thread-safe reference-equality semantics, you must either use synchronized blocks, a custom concurrent implementation using CAS and open addressing, or a third-party library like JCTools' NonBlockingHashMap. There's no ConcurrentIdentityHashMap in the standard library.
Production Insight
Concurrent access to IdentityHashMap can corrupt the linear-probing array — entries may vanish, duplicate, or cause infinite loops.
Always synchronize externally if multiple threads read/write.
For iteration, wrap in synchronized block — not just the map object but the iteration code.
Key Takeaway
IdentityHashMap is single-threaded only, like HashMap.
For concurrent use, synchronize manually or use java.util.concurrent structures with custom equals logic.
Pre-size the map to avoid resizes — use new IdentityHashMap<>(expectedSize).

IdentityHashMap vs WeakHashMap vs EnumMap — When to Use Which

Java provides several specialized Map implementations for specific semantics. Choosing wrongly leads to memory leaks, wasted cycles, or just incorrect behaviour.

WeakHashMap uses WeakReference keys — entries are automatically removed when the key is no longer strongly reachable. Perfect for caches where you want keys to be garbage collected. But WeakHashMap uses equals(), not reference equality. If you need reference-equality with weak keys, you must use a third-party library like Guava's Interners or a custom ReferenceIdentityHashMap.

EnumMap is for enum keys — uses ordinal-based array lookup, ridiculously fast and compact. It enforces enum type safety. It uses equals() (but enums are naturally singleton by reference, so == and equals() are equivalent). EnumMap is almost always the best choive for enum keys.

IdentityHashMap is for reference-equality keys only — not for weak references, not for enums. Use it when you must distinguish object instances by identity. For anything else, pick WeakHashMap for weak keys, EnumMap for enum keys, and HashMap for everything else.

MapSpecializedUsage.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.*;
import java.util.WeakHashMap;

public class MapSpecializedUsage {

    enum Day { MON, TUE, WED, THU, FRI }

    public static void main(String[] args) {

        // 1. IdentityHashMap for reference-equality tracking
        IdentityHashMap<String, String> identityCache = new IdentityHashMap<>();
        String s1 = new String("unique");
        String s2 = new String("unique");
        identityCache.put(s1, "data1");
        identityCache.put(s2, "data2");
        System.out.println("IdentityHashMap size: " + identityCache.size()); // 2

        // 2. WeakHashMap for memory-sensitive caches (but uses equals!)
        WeakHashMap<Object, String> weakCache = new WeakHashMap<>();
        Object key = new Object();
        weakCache.put(key, "value");
        System.out.println("WeakHashMap before GC: " + weakCache.size()); // 1
        key = null; // make key weakly reachable
        System.gc(); // encourage collection (not guaranteed)
        // After GC, the entry may be removed.
        System.out.println("WeakHashMap after GC (likely 0): " + weakCache.size()); // 0 or 1

        // 3. EnumMap for enum keys — fastest and safest
        EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
        schedule.put(Day.MON, "Work");
        schedule.put(Day.FRI, "Party");
        System.out.println("EnumMap size: " + schedule.size()); // 2

        // Contrast: HashMap with enum keys works but slower
        Map<Day, String> hashMap = new HashMap<>();
        hashMap.put(Day.MON, "Work");
        System.out.println("HashMap with enum keys: " + hashMap.get(Day.MON));
    }
}
Output
IdentityHashMap size: 2
WeakHashMap before GC: 1
WeakHashMap after GC (likely 0): 0
EnumMap size: 2
HashMap with enum keys: Work
Mental model: Choose your Map by key semantics, not by hash
  • Reference identity (==) → IdentityHashMap
  • Logical equality (equals) + general → HashMap (or LinkedHashMap for order)
  • Logical equality + weak keys → WeakHashMap
  • Enum keys → EnumMap (always prefer over HashMap)
  • Concurrent access → ConcurrentHashMap (uses equals())
Production Insight
WeakHashMap entries vanish unpredictably after GC — never use for critical data that must persist.
EnumMap is a no-brainer for enum keys: compact, fast, type-safe.
IdentityHashMap's reference equality can cause baffling behavior if you mix with interned strings or autoboxed integers — test with non-interned, non-cached instances.
Key Takeaway
Pick Map implementation based on key semantics, not habit.
IdentityHashMap for reference identity, WeakHashMap for weak keys, EnumMap for enums.
Using the wrong one introduces subtle bugs that are hard to reproduce.

The Hierarchy — Why It Inherits HashMap's Problems Without Its Safety

IdentityHashMap extends AbstractMap and implements Map, Serializable, and Cloneable. That looks like HashMap's pedigree. It's not. The class breaks the Map contract deliberately — using reference equality (==) instead of equals() for key comparisons. This means any code that expects Map's standard behavior (like serialization frameworks, ORMs, or caching layers) will silently corrupt your state. Spring Boot's Jackson autoconfiguration, for example, will explode if your IdentityHashMap contains wrapper keys like Integer or String. The serialized output loses entries because two "same" strings with different references become distinct keys on deserialization. Your microservice sends one map, receives a different one. No error. No warning. Just a bug that manifests in production at 3 AM. Know your inheritance — IdentityHashMap inherits from AbstractMap, not HashMap. That distinction matters when your debugger shows a stack trace through AbstractMap.put.

IdentityMapSerializationBug.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge
import java.io.*;
import java.util.*;

public class IdentityMapSerializationBug {
    public static void main(String[] args) throws Exception {
        IdentityHashMap<String, String> map = new IdentityHashMap<>();
        map.put(new String("key"), "value1");
        map.put(new String("key"), "value2");
        System.out.println("Before: " + map.size()); // 2

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(map);
        oos.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        IdentityHashMap<String, String> restored = (IdentityHashMap<String, String>) ois.readObject();
        ois.close();

        System.out.println("After: " + restored.size()); // 1 — different reference now
    }
}
Output
Before: 2
After: 1
Production Trap:
Never use IdentityHashMap with serialization in distributed systems. The map size will shrink on every serialization round-trip unless you control object identity. Stick to HashMap if you need to send data across services.
Key Takeaway
IdentityHashMap inherits from AbstractMap, but violates its contract — always check serialization compatibility before using it in any IO path.

Constructors — Three Ways to Shoot Yourself in the Foot

IdentityHashMap offers three constructors. Default: starts with an expected maximum size of 21. Parameterized: you pick the expected size, but the internal array always resizes to powers of two. Copy constructor: takes a Map and converts it — but here's the trap. The copy constructor uses the source map's size as expected maximum, ignoring the actual identity semantics. Feed a HashMap with 10,000 String keys to IdentityHashMap via copy constructor, and every string becomes a distinct key by reference identity, not equality. That HashMap you thought had duplicates? IdentityHashMap now treats them as unique entries, ballooning memory. Always use the constructor with expectedMaxSize set to N + (N/3) for optimal performance. The default growth factor of 64 entries per resize is fine for small caches, but for anything above 100 keys, specify the size explicitly. That resize penalty kills throughput in tight loops — I've seen 200ms operations balloon to 2 seconds because of 15 resizes in one putAll call.

IdentityMapConstructorTrap.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge
import java.util.*;

public class IdentityMapConstructorTrap {
    public static void main(String[] args) {
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put("same", "a");
        hashMap.put("same", "b");
        System.out.println("HashMap size: " + hashMap.size()); // 1

        // Copy constructor — strings become distinct by reference
        IdentityHashMap<String, String> identityMap = new IdentityHashMap<>(hashMap);
        System.out.println("IdentityHashMap size: " + identityMap.size()); // 2
        
        // Explicit expected size prevents resize thrashing
        IdentityHashMap<String, String> sizedMap = new IdentityHashMap<>(256);
        System.out.println("Pre-sized map created with optimal capacity");
    }
}
Output
HashMap size: 1
IdentityHashMap size: 2
Pre-sized map created with optimal capacity
Pro Tip:
Use IdentityHashMap(int expectedMaxSize) and set it to (expected count * 4/3). This internal array uses linear probing, so underfilled arrays reduce collision chains. Never trust the default size for collections above 50 entries.
Key Takeaway
Always specify expectedMaxSize explicitly. The copy constructor from a HashMap doubles your keys. Pre-size to N + (N/3) to avoid exponential resize cost.

Operations That Bite — Removing and Accessing by Reference

IdentityHashMap exposes the standard Map API: put, remove, get, keySet, entrySet, values. But each call uses reference identity (==) internally, not equals(). This sounds academic until you debug why a cached object lookup returns null. The remove operation: map.remove(myKey) only works if myKey is the exact same object reference used during put. A deserialized object with identical content will not match. The get operation: same rule. If you use String literals or integers, you're safe because the JVM interns them. But new String("key") creates a different reference every time. In Spring Boot, if you stash request-scoped beans as keys, every request creates new references — your IdentityHashMap grows unboundedly. Iteration: use forEach or entrySet. The internal structure is an Object[] array alternating key, value, key, value — not Node<K,V> objects. This makes entrySet().iterator faster than HashMap for read-heavy workloads. But modifying during iteration throws ConcurrentModificationException same as HashMap. Don't iterate and remove in the same loop without using iterator.remove().

IdentityMapReferenceLookup.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge
import java.util.*;

public class IdentityMapReferenceLookup {
    public static void main(String[] args) {
        IdentityHashMap<String, Integer> map = new IdentityHashMap<>();
        String key1 = new String("account");
        String key2 = new String("account");
        
        map.put(key1, 100);
        System.out.println("Get with key1: " + map.get(key1)); // 100
        System.out.println("Get with key2: " + map.get(key2)); // null — different reference

        // Safe pattern: keep the original reference
        map.put(key1, 200);
        System.out.println("After update: " + map.get(key1)); // 200

        // Iteration with safe removal
        Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            it.next();
            it.remove(); // safe
        }
        System.out.println("Size after removal: " + map.size()); // 0
    }
}
Output
Get with key1: 100
Get with key2: null
After update: 200
Size after removal: 0
Production Trap:
Using new String() as keys in IdentityHashMap creates a memory leak in any loop. Each iteration generates a new reference that stays in the map forever. Always intern strings or retain the original reference to prevent unbounded growth.
Key Takeaway
get(), remove(), containKey() all use reference identity. Store the key reference yourself or use interned literals. New objects = null lookups every time.
● Production incidentPOST-MORTEMseverity: high

Serialization Framework Breaks on Deep Object Graphs

Symptom
Serialization output was incomplete — some objects were missing from the stream, no errors or exceptions. The library silently skipped over certain nodes in the graph.
Assumption
The team assumed that because IdentityHashMap implements Map, it could be passed to any method accepting Map. They didn't read the javadoc that warns about its intentional contract violation.
Root cause
The utility method serializeReference(Object obj, Map<Object, Boolean> visited) created a new key lookup using SerializationKey.of(obj) — a wrapper that overrides equals() by object content. In a normal Map this works, but IdentityHashMap uses ==, so the freshly created wrapper never matched the original reference already stored. The visited map always returned false, causing infinite recursion until a stack overflow was caught, but the catch block skipped the object.
Fix
Typed the parameter as Map<Object, Boolean> but passed the IdentityHashMap. Fix: never widen IdentityHashMap to Map. Use a dedicated wrapper class that enforces reference equality via a private IdentityHashMap inside, or document the reference-equality contract explicitly.
Key lesson
  • IdentityHashMap intentionally violates the Map.equals() contract — never treat it as a general-purpose Map.
  • When a method accepts Map<K,V>, assume it uses equals()-based semantics unless documented otherwise.
  • Audit every call site where an IdentityHashMap is passed to external code. A two-minute check saves a two-day debug.
Production debug guideSymptom → Action guide for reference-equality map pitfalls4 entries
Symptom · 01
ContainsKey returns false for a key you just put in
Fix
Check if you're constructing the key with a new object (e.g., new String(...) vs literal). Use == to compare references. Confirm the exact reference is stored and retrieved.
Symptom · 02
Map size is unexpectedly large with integer keys
Fix
Verify if Integer keys are outside the -128..127 cache range. Integer.valueOf(1000) creates a new object each time. Hold onto the original reference for lookups.
Symptom · 03
Two 'identical' string keys produce two entries
Fix
Check if strings are interned. Literals are always the same reference; new String() creates distinct references. Use System.identityHashCode to compare.
Symptom · 04
Passing IdentityHashMap to library code causes silent skips or duplicates
Fix
Audit all call sites where the map is widened to Map<,>. Replace with typed IdentityHashMap and refactor library calls to accept reference-equality maps or use a wrapper.
★ Quick Debug Commands for IdentityHashMapRun these checks when you suspect reference-equality confusion in production.
Lookup returns null despite key being present
Immediate action
Check if key is the exact same reference by printing System.identityHashCode(key) for both put and get.
Commands
System.out.println(System.identityHashCode(putKey) + " vs " + System.identityHashCode(getKey));
System.out.println(putKey == getKey); // should be true
Fix now
Hold onto the original reference used for put; never reconstruct the key from external data.
Map size grows unexpectedly with integer keys+
Immediate action
Check if Integer keys are outside cache range (-128 to 127).
Commands
Integer a = Integer.valueOf(1000); Integer b = Integer.valueOf(1000); System.out.println(a == b); // false
Check map size before and after second put: System.out.println(map.size());
Fix now
Use a single Integer reference for all operations, or switch to HashMap if value-based equality is intended.
IdentityHashMap vs Other Map Implementations
Feature / AspectHashMapIdentityHashMapWeakHashMapEnumMap
Key equality checkkey.equals(otherKey)key == otherKey (reference only)key.equals(otherKey) (but weak references)key.equals(otherKey) (enum singletons)
Hash functionkey.hashCode() + spreadSystem.identityHashCode(key)key.hashCode()ordinal-based index
Internal structureArray of Node lists / tree binsSingle flat Object[] (open addressing)Array of Entry with WeakReference keysInternal Object[] array indexed by ordinal
Per-entry allocationYes — one Node per entryNo — entries in flat arrayYes — one Entry per entryYes — one entry per enum constant (pre-allocated?)
Null keysYes (one)Yes (one)Yes (one)No (enum class defines keys)
Thread safetyNot thread-safeNot thread-safeNot thread-safeNot thread-safe
Best forGeneral-purpose key-value storageObject graph tracking, proxies, serializationCaches where keys can be garbage collectedEnum-based lookup/configuration
Key lifetimeStrong referenceStrong referenceWeak reference (auto-remove when no strong ref)Strong reference (enum constants never GC'd)
PerformanceO(1) avg, O(log n) worst for tree binsO(1) avg, O(n) worst for linear probingO(1) avg, O(log n) worstO(1) guaranteed (ordinal index)

Key takeaways

1
IdentityHashMap uses == not equals() for key comparison
two objects that look identical but are different heap instances are treated as completely separate keys, which is the entire point of the class.
2
Its internal structure is a single flat Object[] with keys and values interleaved
no Entry node allocation, which gives better cache locality and less GC pressure for maps in the dozens-to-hundreds of entries range.
3
Autoboxing with Integer/Long keys outside the -128..127 cache range creates new object instances on every valueOf() call
lookups with a freshly constructed equal Integer will always return null because the reference doesn't match.
4
IdentityHashMap intentionally violates the Map interface contract around equals()
never pass it to library or framework code that assumes standard Map semantics, or you'll get silent, maddening bugs.
5
Use IdentityHashMap exclusively for reference-equality scenarios
graph traversal, proxy wrappers, serialization back-references. For everything else, use the appropriate Map implementation (HashMap, WeakHashMap, EnumMap).

Common mistakes to avoid

4 patterns
×

Using IdentityHashMap as a drop-in HashMap replacement

Symptom
Lookups return null even though you 'put' the key, because you're retrieving with a different object instance that happens to be equal by value.
Fix
Only use IdentityHashMap when you explicitly need reference equality; always retrieve with the exact same reference you inserted. Never pass it to code expecting standard Map semantics.
×

Passing IdentityHashMap to library code that expects standard Map semantics

Symptom
Silent incorrect behaviour — library code fails to find keys it just inserted, or treats equal objects as distinct entries in ways the library never anticipated.
Fix
Never widen an IdentityHashMap to Map<K,V> and hand it off to third-party or framework code unless you've audited exactly how that code uses the map. Use a local variable typed as IdentityHashMap to make the intent visible.
×

Autoboxing Integer/Long keys without holding the original reference

Symptom
Integer values outside -128..127 produce two separate entries for the 'same' number, and subsequent lookups return null because the lookup creates yet another new Integer instance.
Fix
Store the boxed reference in a variable immediately after construction (Integer key = Integer.valueOf(largeNumber)), always use that variable for both put and get, and never reconstruct the Integer inline at the call site.
×

Assuming String literals behave consistently as IdentityHashMap keys

Symptom
Lookups for a literal string work fine, but lookups with the same string from user input (non-interned) return null. The map appears to lose entries.
Fix
Understand that string literals are interned (same reference), but dynamically created strings are not. If you need reference equality with strings, always use new String(...) in tests and be aware that production data may not be interned.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
HashMap and IdentityHashMap both implement Map — what's the fundamental ...
Q02SENIOR
Can you describe a concrete scenario in production code where using Hash...
Q03SENIOR
IdentityHashMap uses a flat Object array instead of Entry nodes — what a...
Q01 of 03JUNIOR

HashMap and IdentityHashMap both implement Map — what's the fundamental difference in how they determine whether two keys are the same, and what does that mean for the hashCode() contract?

ANSWER
HashMap uses equals() and hashCode() — two objects are the same key if they are logically equal. IdentityHashMap uses == and System.identityHashCode() — two objects are the same key only if they are the exact same heap instance. This means IdentityHashMap doesn't require keys to override hashCode() or equals() at all; it relies on the JVM's identity hash code. The Map interface contract says keys should be compared by equals(), so IdentityHashMap intentionally violates that contract. That's documented but can surprise callers.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between IdentityHashMap and HashMap in Java?
02
Does IdentityHashMap allow null keys and null values?
03
Is IdentityHashMap thread-safe?
04
How does IdentityHashMap handle resizing?
05
Can I create a reference-equality Set in Java?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Collections. Mark it forged?

9 min read · try the examples if you haven't

Previous
WeakHashMap in Java
17 / 21 · Collections
Next
Java flatMap(): Flatten Streams and Optional