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.
thecodeforge.io
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;
publicclassIdentityHashMapInternals {
publicstaticvoidmain(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 = newString("session");
String keyB = newString("session");
System.out.println("--- Equality checks ---");
System.out.println("keyA.equals(keyB) : " + keyA.equals(keyB)); // true — same contentSystem.out.println("keyA == keyB : " + (keyA == keyB)); // false — different objectsSystem.out.println("identityHashCode A : " + System.identityHashCode(keyA));
System.out.println("identityHashCode B : " + System.identityHashCode(keyB));
// ------- IdentityHashMap behaviour -------IdentityHashMap<String, String> identityMap = newIdentityHashMap<>();
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-ASystem.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 OVERWRITESSystem.out.println("\n--- HashMap (for comparison) ---");
System.out.println("Size (expect 1) : " + hashMap.size()); // only 1 entry survivesSystem.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;
/**
* DemonstratesIdentityHashMap as a 'visited' tracker during
* recursive object graph traversal. This is the pattern Java's
* own serialization engine uses internally.
*/
publicclassObjectGraphVisitor {
// 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.staticclassGraphNode {
String label;
List<GraphNode> neighbours = newArrayList<>();
GraphNode(String label) { this.label = label; }
// equals() and hashCode() are based on label content — NOT reference
@Overridepublicbooleanequals(Object other) {
if (!(other instanceofGraphNode)) returnfalse;
returnthis.label.equals(((GraphNode) other).label);
}
@OverridepublicinthashCode() { return label.hashCode(); }
@OverridepublicStringtoString() { 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.privatefinalIdentityHashMap<GraphNode, Boolean> visited = newIdentityHashMap<>();
publicvoidtraverse(GraphNode startNode) {
// containsKey uses == internally — correct for cycle detectionif (visited.containsKey(startNode)) {
System.out.println("Already visited (by reference): " + startNode);
return;
}
visited.put(startNode, Boolean.TRUE); // mark this exact object as seenSystem.out.println("Visiting: " + startNode);
for (GraphNode neighbour : startNode.neighbours) {
traverse(neighbour); // recurse into neighbours
}
}
publicstaticvoidmain(String[] args) {
GraphNode nodeA = newGraphNode("auth-service");
GraphNode nodeB = newGraphNode("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 instanceSystem.out.println("nodeA.equals(nodeC) = " + nodeA.equals(nodeC)); // true — same label!System.out.println("nodeA == nodeC = " + (nodeA == nodeC)); // false — different objectsSystem.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.newObjectGraphVisitor().traverse(nodeA);
}
}
💡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.
thecodeforge.io
Identityhashmap Java
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.
*/
publicclassAutoboxingTrap {
publicstaticvoidmain(String[] args) {
IdentityHashMap<Integer, String> sessionMap = newIdentityHashMap<>();
// 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 instanceInteger sessionIdSmall2 = Integer.valueOf(42); // returns the SAME cached instanceSystem.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 1System.out.println("Mapsize (expect 1 due to cache): " + sessionMap.size()); // 1System.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 objectInteger sessionIdLarge2 = Integer.valueOf(1000); // another new objectSystem.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("Mapsize (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;
publicclassIdentityHashMapThreadSafety {
publicstaticvoidmain(String[] args) throwsInterruptedException {
// NOT thread-safe — concurrent put may cause infinite loop or lost updatesIdentityHashMap<String, String> unsafe = newIdentityHashMap<>();
// Thread 1: puts 1000 entriesThread t1 = newThread(() -> {
for (int i = 0; i < 1000; i++) {
// Use new Strings to avoid interning — distinct references
unsafe.put(newString("key" + i), "value-" + i);
}
});
// Thread 2: iterates concurrentlyThread t2 = newThread(() -> {
for (int i = 0; i < 1000; i++) {
// May throw ConcurrentModificationException or see inconsistent statefor (Map.Entry<String, String> e : unsafe.entrySet()) {
// simulate readString 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 iterationMap<String, String> safe = Collections.synchronizedMap(newIdentityHashMap<>());
// Example of correct iteration with explicit syncsynchronized (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;
publicclassMapSpecializedUsage {
enumDay { MON, TUE, WED, THU, FRI }
publicstaticvoidmain(String[] args) {
// 1. IdentityHashMap for reference-equality trackingIdentityHashMap<String, String> identityCache = newIdentityHashMap<>();
String s1 = newString("unique");
String s2 = newString("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 = newWeakHashMap<>();
Object key = newObject();
weakCache.put(key, "value");
System.out.println("WeakHashMap before GC: " + weakCache.size()); // 1
key = null; // make key weakly reachableSystem.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 safestEnumMap<Day, String> schedule = newEnumMap<>(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 slowerMap<Day, String> hashMap = newHashMap<>();
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
Mental model: Choose your Map by key semantics, not by hash
The kind of key determines which Map implementation to use — start with key equality semantics, then optimize.
Reference identity (==) → IdentityHashMap
Logical equality (equals) + general → HashMap (or LinkedHashMap for order)
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.
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.
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.thecodeforgeimport java.util.*;
publicclassIdentityMapReferenceLookup {
publicstaticvoidmain(String[] args) {
IdentityHashMap<String, Integer> map = newIdentityHashMap<>();
String key1 = newString("account");
String key2 = newString("account");
map.put(key1, 100);
System.out.println("Get with key1: " + map.get(key1)); // 100System.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 removalIterator<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 / Aspect
HashMap
IdentityHashMap
WeakHashMap
EnumMap
Key equality check
key.equals(otherKey)
key == otherKey (reference only)
key.equals(otherKey) (but weak references)
key.equals(otherKey) (enum singletons)
Hash function
key.hashCode() + spread
System.identityHashCode(key)
key.hashCode()
ordinal-based index
Internal structure
Array of Node lists / tree bins
Single flat Object[] (open addressing)
Array of Entry with WeakReference keys
Internal Object[] array indexed by ordinal
Per-entry allocation
Yes — one Node per entry
No — entries in flat array
Yes — one Entry per entry
Yes — one entry per enum constant (pre-allocated?)
Null keys
Yes (one)
Yes (one)
Yes (one)
No (enum class defines keys)
Thread safety
Not thread-safe
Not thread-safe
Not thread-safe
Not thread-safe
Best for
General-purpose key-value storage
Object graph tracking, proxies, serialization
Caches where keys can be garbage collected
Enum-based lookup/configuration
Key lifetime
Strong reference
Strong reference
Weak reference (auto-remove when no strong ref)
Strong reference (enum constants never GC'd)
Performance
O(1) avg, O(log n) worst for tree bins
O(1) avg, O(n) worst for linear probing
O(1) avg, O(log n) worst
O(1) guaranteed (ordinal index)
⚙ Quick Reference
8 commands from this guide
File
Command / Code
Purpose
IdentityHashMapInternals.java
public class IdentityHashMapInternals {
How IdentityHashMap Works Internally
ObjectGraphVisitor.java
/**
When IdentityHashMap Is the Right Tool
AutoboxingTrap.java
/**
Performance Profile, Gotchas, and the Intentional Contract V
IdentityHashMapThreadSafety.java
public class IdentityHashMapThreadSafety {
Thread Safety, Iteration, and Memory Considerations
MapSpecializedUsage.java
public class MapSpecializedUsage {
IdentityHashMap vs WeakHashMap vs EnumMap
IdentityMapSerializationBug.java
public class IdentityMapSerializationBug {
The Hierarchy
IdentityMapConstructorTrap.java
public class IdentityMapConstructorTrap {
Constructors
IdentityMapReferenceLookup.java
public class IdentityMapReferenceLookup {
Operations That Bite
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.
Q02 of 03SENIOR
Can you describe a concrete scenario in production code where using HashMap instead of IdentityHashMap would introduce a bug that's very hard to find? Walk me through why it fails.
ANSWER
Consider a cycle detection algorithm in a serialization library. You visit objects in a graph and mark them in a visited set. If your domain objects override equals() (e.g., a customer with an ID), then two different customer instances with the same ID are considered 'equal'. Using HashMap for the visited set, when you encounter the second instance, you incorrectly treat it as already visited and skip it, losing data. The bug manifests as missing objects in the output with no error. Only when you deep-inspect the serialized stream or add logging do you realize the visited set was wrong. IdentityHashMap fixes this because each object is a unique heap instance regardless of its fields.
Q03 of 03SENIOR
IdentityHashMap uses a flat Object array instead of Entry nodes — what are the memory and cache-locality implications of that design, and why does it fix the load factor at 2/3 rather than letting you configure it?
ANSWER
The flat Object[] stores keys and values interleaved: index 2i holds key, 2i+1 holds value. There are no Entry objects, so each entry saves ~24 bytes of object overhead (object header and reference fields) and reduces GC pressure. The interleaved layout also improves CPU cache locality because the key and value for the same entry are adjacent in memory. The load factor is fixed at 2/3 (rather than configurable) because open addressing with linear probing degrades rapidly above about 0.7 load — they chose a safe default that balances space and probe length. Allowing configuration would risk users setting it too high and destroying performance. Also, because the array must be a power of two for bitwise hashing, the 2/3 threshold ensures efficient resize timing.
01
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?
JUNIOR
02
Can you describe a concrete scenario in production code where using HashMap instead of IdentityHashMap would introduce a bug that's very hard to find? Walk me through why it fails.
SENIOR
03
IdentityHashMap uses a flat Object array instead of Entry nodes — what are the memory and cache-locality implications of that design, and why does it fix the load factor at 2/3 rather than letting you configure it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between IdentityHashMap and HashMap in Java?
HashMap compares keys using equals() and hashCode(), so two different objects with the same logical value count as the same key. IdentityHashMap compares keys using == (reference equality) and System.identityHashCode(), so only the exact same heap object counts as a match. This makes IdentityHashMap ideal for tracking specific object instances rather than logically equal values.
Was this helpful?
02
Does IdentityHashMap allow null keys and null values?
Yes. IdentityHashMap allows one null key and any number of null values, the same as HashMap. The null key is handled as a special case internally since System.identityHashCode(null) returns 0.
Was this helpful?
03
Is IdentityHashMap thread-safe?
No — IdentityHashMap is not thread-safe and must be externally synchronized if accessed from multiple threads concurrently. You can wrap it with Collections.synchronizedMap(), though for most concurrent use cases you'd want ConcurrentHashMap instead — which uses equals() semantics. There is no concurrent identity-map in the standard library, so you'd need to synchronize manually or use a lock-striped approach.
Was this helpful?
04
How does IdentityHashMap handle resizing?
It doubles the internal array when the map exceeds 2/3 full. The size is always a power of two. The array length is 2 * capacity (keys and values interleaved). So a default map (capacity 32) has an array of length 64. Resize recalculates positions for all existing entries using linear probing.
Was this helpful?
05
Can I create a reference-equality Set in Java?
Yes — use Collections.newSetFromMap(new IdentityHashMap<>()). This gives you a Set that uses reference equality for membership, equivalent to an IdentityHashSet. There's no dedicated class in the JDK, but this one-liner works perfectly and is clearly intentional.