Wrapper Keys Cause IdentityHashMap Serialization Loss
ContainsKey false, duplicated integer keys, identical strings map twice—all from IdentityHashMap's ==.
- 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
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.
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.
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.
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.
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.
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.
| 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) |
Key Takeaways
- 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. - 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.
- 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.
- 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. - 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
- 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 Questions on This Topic
- QHashMap 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?JuniorReveal
- QCan 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.SeniorReveal
- QIdentityHashMap 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?Mid-levelReveal
Frequently Asked Questions
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.
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.
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.
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.
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.
That's Collections. Mark it forged?
5 min read · try the examples if you haven't