Contents

In a regular HashMap, the map holds a strong reference to each key, preventing garbage collection. WeakHashMap wraps each key in a WeakReference instead — once no other strong reference to the key exists, the GC can collect it and the map entry disappears automatically:

import java.util.*; // WeakHashMap — keys are stored as WeakReferences // When a key has no other strong references, the GC can collect it // and the entry is automatically removed from the map WeakHashMap<Object, String> weakMap = new WeakHashMap<>(); Object key1 = new Object(); Object key2 = new Object(); weakMap.put(key1, "value for key1"); weakMap.put(key2, "value for key2"); System.out.println(weakMap.size()); // 2 // Releasing the strong reference — key2 can now be GC'd key2 = null; // Force a GC for demonstration (not reliable in production) System.gc(); // After GC, the entry for the nulled key may be gone // (not guaranteed immediately, but will happen eventually) System.out.println("After GC, size: " + weakMap.size()); // 1 or 2 depending on GC // key1 is still strongly referenced — its entry stays System.out.println(weakMap.containsValue("value for key1")); // true // String literals are interned and held by the JVM — they are NEVER collected // Use WeakHashMap only with non-interned objects as keys WeakHashMap<String, String> badIdea = new WeakHashMap<>(); badIdea.put("literal", "metadata"); // "literal" is interned; entry never evicted String dynamic = new String("dynamic"); // new String() — not interned badIdea.put(dynamic, "metadata2"); dynamic = null; // now collectible System.gc(); // After GC: "dynamic" entry may be gone, "literal" stays WeakHashMap entries can disappear at any time — even between two consecutive lines of code — because GC can run at any point. Never rely on an entry still being present after a GC-eligible key was released.

Eviction is lazy: entries are not removed immediately when a key is collected. Internally, WeakHashMap uses a ReferenceQueue — when a key is GC'd, its WeakReference is enqueued, and the next call to any map method triggers a sweep that removes those stale entries:

import java.lang.ref.*; import java.util.*; // How WeakHashMap eviction works internally: // 1. Keys are wrapped in WeakReference objects backed by a ReferenceQueue // 2. When the GC collects a key, the WeakReference is enqueued // 3. On the next map operation (get/put/size/etc.), the map polls the queue // and removes entries whose keys were collected // Demonstrating the timing of eviction WeakHashMap<Object, String> map = new WeakHashMap<>(); for (int i = 0; i < 5; i++) { map.put(new Object(), "value-" + i); // all keys immediately eligible for GC } System.out.println("Before GC: " + map.size()); // 5 (keys still in scope) // The keys created in the loop have no strong references — they can be GC'd now System.gc(); Thread.sleep(100); // give GC a chance to run // Trigger eviction by calling any map method int sizeAfterGc = map.size(); // this polls the internal ReferenceQueue System.out.println("After GC: " + sizeAfterGc); // likely 0 // Practical rule: never use WeakHashMap if keys need to stay alive // Do use it when keys are objects whose lifetime is managed elsewhere // Example: canonical map pattern (intern pool) // Maps each object to a canonical representative — GC removes stale entries Map<String, WeakReference<String>> internPool = new WeakHashMap<>(); String s1 = new String("hello"); internPool.put(s1, new WeakReference<>(s1)); WeakReference<String> ref = internPool.get(s1); System.out.println(ref != null ? ref.get() : "evicted"); // hello // WeakHashMap does NOT support null values cleanly for eviction detection // because null is also the return value for a missing key; use a sentinel object final Object PRESENT = new Object(); WeakHashMap<Object, Object> weakSet = new WeakHashMap<>();

The two canonical use cases are metadata caches (attach computed properties to objects without preventing their collection) and weak listener registries (avoid the classic listener-leak where an event source's strong reference to a listener prevents it from being GC'd):

import java.util.*; import java.util.function.*; // --- Use Case 1: Metadata / attribute cache --- // Attach computed metadata to objects without preventing their GC WeakHashMap<Object, Map<String, Object>> metadataCache = new WeakHashMap<>(); class ImageProcessor { private static final WeakHashMap<byte[], Map<String, Object>> cache = new WeakHashMap<>(); static Map<String, Object> analyze(byte[] imageData) { return cache.computeIfAbsent(imageData, data -> { // Expensive analysis — cached per image data reference Map<String, Object> result = new HashMap<>(); result.put("width", 100); result.put("height", 200); result.put("size", data.length); return result; }); } } byte[] img = new byte[1024]; Map<String, Object> info = ImageProcessor.analyze(img); System.out.println(info); // {width=100, height=200, size=1024} // When 'img' is no longer referenced elsewhere, the cache entry is automatically evicted // No manual cache.remove(img) needed // --- Use Case 2: Weak listener registry --- // Listeners registered here are auto-removed when the listener object is GC'd // Prevents the classic "listener leak" pattern class EventBus { private final WeakHashMap<Object, Runnable> listeners = new WeakHashMap<>(); public void register(Object owner, Runnable listener) { listeners.put(owner, listener); } public void fire() { // Iterate a snapshot to avoid ConcurrentModificationException during GC eviction new ArrayList<>(listeners.values()).forEach(Runnable::run); } } EventBus bus = new EventBus(); Object componentA = new Object(); bus.register(componentA, () -> System.out.println("ComponentA handling event")); Object componentB = new Object(); bus.register(componentB, () -> System.out.println("ComponentB handling event")); bus.fire(); // both listeners fire componentB = null; // componentB is released System.gc(); Thread.sleep(50); bus.fire(); // only componentA's listener fires (componentB was GC'd) The listener-leak problem — where objects that register callbacks are never garbage collected because the event source holds a strong reference to them — is one of the most common memory leaks in Java GUI and observer-pattern code. WeakHashMap solves it elegantly when the listener's identity is used as the map key.

Java provides four reference strengths. WeakReference is collected on the next GC cycle once no strong or soft reference exists; SoftReference is collected only under memory pressure; PhantomReference is used for post-collection cleanup via a ReferenceQueue:

import java.lang.ref.*; // Java has four reference strengths: // Strong — normal references (Object o = new Object()) — GC never collects // Soft — SoftReference — GC collects only under memory pressure // Weak — WeakReference — GC collects as soon as no strong/soft reference exists // Phantom — PhantomReference — notification after collection, no .get() possible // WeakReference basics Object strongRef = new Object(); WeakReference<Object> weakRef = new WeakReference<>(strongRef); System.out.println(weakRef.get() != null); // true — strongly reachable strongRef = null; // release strong reference System.gc(); // weakRef.get() MAY now return null System.out.println(weakRef.get()); // null (likely after GC) // ReferenceQueue — notified when a reference is cleared ReferenceQueue<Object> refQueue = new ReferenceQueue<>(); Object tracked = new Object(); WeakReference<Object> trackedRef = new WeakReference<>(tracked, refQueue); tracked = null; System.gc(); Reference<?> polled = refQueue.poll(); // non-blocking poll if (polled != null) { System.out.println("Object was collected, reference enqueued"); } // SoftReference — useful for memory-sensitive caches // Kept as long as JVM has enough heap; cleared before OutOfMemoryError SoftReference<byte[]> softCache = new SoftReference<>(new byte[1024 * 1024]); byte[] cached = softCache.get(); if (cached == null) { // Cache miss — memory was reclaimed under pressure cached = new byte[1024 * 1024]; // reload softCache = new SoftReference<>(cached); } // Reference strength summary (what triggers GC collection): // Strong → never collected while reference is in scope // Soft → collected only when JVM needs memory (good for caches) // Weak → collected on any GC cycle (good for canonicalizing, metadata) // Phantom → collected; .get() always null; used for cleanup tracking

IdentityHashMap intentionally violates the Map contract by using == (reference equality) instead of equals() for key comparison. Two distinct objects with the same content are treated as different keys, making it the right tool whenever object identity — not logical equality — is the correct semantic:

import java.util.*; // IdentityHashMap uses System.identityHashCode() and == for key comparison // NOT equals() and hashCode() — this violates Map's general contract intentionally IdentityHashMap<String, String> idMap = new IdentityHashMap<>(); String s1 = new String("key"); String s2 = new String("key"); // different object, same content // HashMap: s1 and s2 are the same key (equals() returns true) HashMap<String, String> hashMap = new HashMap<>(); hashMap.put(s1, "value1"); hashMap.put(s2, "value2"); // overwrites s1's entry System.out.println(hashMap.size()); // 1 // IdentityHashMap: s1 and s2 are DIFFERENT keys (s1 != s2) idMap.put(s1, "value1"); idMap.put(s2, "value2"); // separate entry System.out.println(idMap.size()); // 2 System.out.println(idMap.get(s1)); // value1 System.out.println(idMap.get(s2)); // value2 // Interned strings: "hello" == "hello" (same reference in string pool) String interned1 = "hello"; String interned2 = "hello"; // same reference idMap.put(interned1, "first"); idMap.put(interned2, "second"); // overwrites — same reference System.out.println(idMap.size()); // 2 (only s1,s2 + 1 interned) // Practical: tracking visited nodes in a graph record Node(int id, List<Node> neighbors) {} Node a = new Node(1, new ArrayList<>()); Node b = new Node(2, new ArrayList<>()); Node c = new Node(3, new ArrayList<>()); a.neighbors().addAll(List.of(b, c)); b.neighbors().add(a); // cycle // Use IdentityHashMap for visited set — tracks object identity, not logical equality void dfs(Node start) { IdentityHashMap<Node, Boolean> visited = new IdentityHashMap<>(); Deque<Node> stack = new ArrayDeque<>(); stack.push(start); while (!stack.isEmpty()) { Node current = stack.pop(); if (visited.containsKey(current)) continue; // identity check — no infinite loop visited.put(current, true); System.out.println("Visiting node " + current.id()); current.neighbors().forEach(stack::push); } } dfs(a); // Visiting node 1 // Visiting node 3 // Visiting node 2

The most common applications are cycle detection during deep-copy or serialization (tracking which exact object instances have been visited), proxy registries (mapping each target object to its proxy by reference), and class-keyed dispatch tables (where class literals are effectively singletons):

import java.util.*; // --- Use Case 1: Serialization / deep-copy cycle detection --- // Track objects already serialized to handle circular references class DeepCopy { private final IdentityHashMap<Object, Object> copies = new IdentityHashMap<>(); @SuppressWarnings("unchecked") public <T> T copy(T original) { if (original == null) return null; // If already copied this exact object instance, return the copy Object existing = copies.get(original); if (existing != null) return (T) existing; // Simulate creating a copy (real impl would use reflection) Object clone = createShallowCopy(original); copies.put(original, clone); // track by identity before recursing // ... recursively copy fields ... return (T) clone; } private Object createShallowCopy(Object o) { return new Object(); /* simplified */ } } // --- Use Case 2: Proxy / instrumentation frameworks --- // Map original objects to their proxy counterparts by identity class ProxyRegistry { private final IdentityHashMap<Object, Object> proxies = new IdentityHashMap<>(); public <T> T getOrCreateProxy(T target, Class<T> iface) { return iface.cast(proxies.computeIfAbsent(target, t -> { // In real code: java.lang.reflect.Proxy.newProxyInstance(...) System.out.println("Creating proxy for " + System.identityHashCode(t)); return t; // simplified — just return same object })); } } ProxyRegistry registry = new ProxyRegistry(); List<String> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(); // different object registry.getOrCreateProxy(list1, List.class); // creates proxy registry.getOrCreateProxy(list1, List.class); // returns cached proxy (same identity) registry.getOrCreateProxy(list2, List.class); // creates new proxy (different identity) // --- Use Case 3: Class-keyed maps (classes are singletons per ClassLoader) --- // class literals are effectively interned; HashMap and IdentityHashMap behave the same // but IdentityHashMap is faster because it skips hashCode() computation IdentityHashMap<Class<?>, String> typeLabels = new IdentityHashMap<>(); typeLabels.put(Integer.class, "int"); typeLabels.put(String.class, "str"); typeLabels.put(Double.class, "dbl"); Object value = 42; System.out.println(typeLabels.get(value.getClass())); // int IdentityHashMap is intentionally designed to violate the Map contract (which requires equals()-based key comparison). Its Javadoc explicitly warns: "This class is not a general-purpose Map implementation." Use it only when reference identity is the correct semantic for your keys.

The three maps differ on two axes: how keys are compared (equals() vs ==) and how keys are referenced (strong vs weak). Use this table to pick the right one for your access pattern:

// | Property | HashMap | WeakHashMap | IdentityHashMap | // |-----------------------|-------------------|----------------------|-----------------------| // | Key comparison | equals() | equals() | == (reference) | // | Key hashing | hashCode() | hashCode() | System.identityHash.. | // | Key reference type | Strong | Weak | Strong | // | GC-driven eviction | No | Yes (when key GC'd) | No | // | Null keys | 1 null allowed | 1 null allowed | 1 null allowed | // | Null values | Yes | Yes | Yes | // | Thread-safe | No | No | No | // | Iteration order | Unspecified | Unspecified | Unspecified | // | Typical use | General purpose | Caches, listeners | Proxy, cycle detect | // | Performance | O(1) | O(1)* | O(1) (no equals call) | // | When key is String | Careful: interned | Careful: interned | Two "a" may differ | // * get/put, but iteration may trigger GC cleanup // Thread-safe wrappers for single-threaded scenarios where you want synchronized access: Map<Object, String> syncWeak = Collections.synchronizedMap(new WeakHashMap<>()); // Note: iteration still requires external synchronization: // synchronized (syncWeak) { syncWeak.forEach(...); } // Key point recap: // WeakHashMap → "I want entries to disappear when the key is no longer used" // IdentityHashMap → "I want two objects with equal content to be separate keys" // HashMap → "Normal key-value storage with logical equality" // Verify IdentityHashMap semantics IdentityHashMap<Object, Integer> idm = new IdentityHashMap<>(); Object x = new Object(); Object y = new Object(); Object z = x; // z and x are the SAME object idm.put(x, 1); idm.put(y, 2); idm.put(z, 3); // z == x → overwrites x's entry System.out.println(idm.size()); // 2 (x/z counted once, y once) System.out.println(idm.get(x)); // 3 (overwritten by z) System.out.println(idm.get(z)); // 3 (z is x) System.out.println(idm.get(y)); // 2