Java Map containsKey — Null-Valued Cache Entry Bug
90% of microservices were incorrectly marked 'not registered' because get() returned null for null-valued keys.
- containsKey() returns true if the key is present, even if its value is null
- map.get(key) != null is NOT equivalent — it treats a null-valued key as absent
- Use getOrDefault() to read in one lookup, not two
- computeIfAbsent() atomically checks and inserts in one call
- HashMap accepts one null key; TreeMap and ConcurrentHashMap throw NPE
- The silent bug: a service registry with null URLs gets skipped if you use get() != null
containsKey() answers: is this key registered in the map? It doesn't tell you what the value is, and it doesn't distinguish between a key mapped to null vs a key that doesn't exist. For most maps that's fine — but in maps where null values are legitimate, the distinction matters.
containsKey() is simple but has a couple of edge cases that produce bugs in production code. The main one: map.get(key) != null is NOT equivalent to map.containsKey(key). If the map contains the key with a null value, get() returns null but containsKey() returns true. When you need to distinguish 'key exists with null value' from 'key doesn't exist', you must use containsKey().
containsKey() Usage and Edge Cases
containsKey() is the standard way to ask 'is this key present?' in any Map implementation. It returns true if the key exists, regardless of the value – even if that value is null. This is the critical distinction from get(key) != null.
- Caches that store null for 'not computed yet' – you need containsKey() to know the key is registered.
- Configuration maps where a key may map to a null value meaning 'default will be used'.
- Service registries where the value is the URL, but the URL is null until deployment.
When you call containsKey() on a HashMap, the map calculates the key's hash, locates the bucket, and checks equals() against each entry in that bucket. For TreeMap, it performs a binary search using compareTo(). For ConcurrentHashMap, it uses a striped read lock and volatile reads for thread safety.
- containsKey() checks only the key set – it tells you whether the key exists, period.
- get() checks the key set first (to find the entry), then returns whatever value is stored, which could be null.
- A null value is a valid entry – it means 'the value for this key is null', not 'no entry'.
- Mistaking
get()!= null for key existence is like checking whether a filing cabinet drawer has a file by looking if the file's contents are non-empty – but the drawer could have an empty file.
get() != null anti-pattern is the #1 cause of silent containsKey bugs.map.get(key) != null and consider replacing it with containsKey(key) where null values are possible.Handling Null Values: containsKey vs get() != null
The most common production bug with containsKey() is using map.get(key) != null as a key existence check. This works fine when the map never contains null values – but the moment a null value is stored, the logic breaks silently.
Consider a feature flag map: Map<String, Boolean> features. If a feature is present but disabled, you might store false. But what about null? Some teams use null to mean 'not evaluated yet'. In that case: - features.get("new-payment") != null → false even though the key exists. - features.containsKey("new-payment") → true – the correct answer.
When you need to handle both absent keys and null values, the pattern is: 1. Use containsKey() to check if the key exists. 2. Then call get() separately to retrieve the value (which may be null). But that's two lookups. A better approach for many cases is to avoid null values in maps altogether – store Optional.empty() or a sentinel value. Java 8's Optional class works well for nullable map values.
get() != null.get() != null as a potential containsKey misuse.get() to retrieve value (or use computeIfAbsent for atomic compute).Modern Alternatives: getOrDefault and computeIfAbsent
Java 8 introduced methods that combine the check and the action into one atomic call. These are almost always better than the classic containsKey() + get() or containsKey() + put() pattern.
- getOrDefault(key, defaultValue): Returns the value mapped to the key, or defaultValue if the key is absent. It does NOT distinguish between 'key exists with null value' and 'key absent' – both return defaultValue. If you need that distinction, stick with containsKey(). But for most use cases, getOrDefault is cleaner and avoids the two-lookup issue.
- computeIfAbsent(key, mappingFunction): If the key is absent (or exists with null value), the mapping function runs and the result is stored and returned. This is perfect for lazy initialisation, caching, and singleton factories. It's atomic – only one thread computes and stores.
- putIfAbsent(key, value): Puts the value only if the key is not already present. Returns the existing value or null. This is simpler than containsKey() +
put()but still involves a potential write.
Which one to choose? If you need the value in all cases, getOrDefault wins. If you need to compute the value only once, use computeIfAbsent. If you just want to set a default without computation, putIfAbsent.
get() with getOrDefault().put() with computeIfAbsent or putIfAbsent.Null Key Support Across Map Implementations
Not every Map implementation handles null keys the same way. This is a common source of unexpected NullPointerExceptions in production, especially when refactoring from HashMap to TreeMap or ConcurrentHashMap.
- HashMap: Allows exactly one null key. It's stored in a special bucket and found via hash 0. containsKey(null) works fine.
- LinkedHashMap: Same as HashMap – allows one null key.
- TreeMap: Throws NullPointerException on any null key (including containsKey(null)). TreeMap uses compareTo() which cannot handle null without a custom Comparator that does.
- ConcurrentHashMap: Does NOT allow null keys. It throws NullPointerException on containsKey(null) and put(null, ...). This is intentional: null keys would break thread-safety guarantees and methods like computeIfAbsent.
- EnumMap: Key must be an enum. Null key throws NullPointerException.
- IdentityHashMap: Allows null keys (stored with special handling).
If you need null key support across different implementations, wrap the key in an Optional (pre-Java 9) or convert null to a sentinel string like "__null__". But the cleanest approach is to avoid null keys altogether.
Performance Considerations of containsKey()
containsKey() is O(1) on average for HashMap, O(log n) for TreeMap, and O(1) amortized for ConcurrentHashMap (though with higher constant overhead due to volatile reads). For most applications, the performance of containsKey() is not a bottleneck.
However, the pattern of containsKey() followed by is TWO hash lookups (or two tree traversals). This doubles the time for that code path. For high-frequency operations (e.g., inside a hot loop, parsing a large config file), that overhead adds up.get()
Synthetic benchmark: On a HashMap with a million entries, containsKey()+get() takes ~120ns on average vs getOrDefault at ~70ns. Not huge, but at 10M calls per second, that's ~500ms difference. In latency-sensitive systems, every microsecond counts.
Additionally, computeIfAbsent may run the function under the map's segment lock (in ConcurrentHashMap), which can stall other operations on the same segment. But in practice, for most workloads, the lock contention is negligible.
- Single-threaded, low-frequency: containsKey + get is fine.
- Single-threaded, high-frequency: prefer getOrDefault or computeIfAbsent.
- Concurrent, high-frequency: use ConcurrentHashMap's computeIfAbsent (it's highly optimised) or use
get()with a retry pattern if null is acceptable.
The Null-Valued Cache Entry That Brought Down Order Processing
get() meant the key was absent, so they used if (map.get(key) != null) to check registration.get() != null, which returned false for null-valued entries.map.containsKey(key) or used getOrDefault(key, DEFAULT_URL) to handle pending URLs explicitly.- Never use
get()!= null to check key existence when the map can hold null values. - Prefer containsKey() to distinguish 'absent key' from 'present with null value'.
- Consider redesigning the map to store Optional or a sentinel instead of null.
get() separately (or use getOrDefault with a fallback that preserves null semantics).if (key != null) before calling containsKey().equals() contracts. If the key is mutable and its state changed after insertion, the map may not find it. Also verify that the map reference hasn't been replaced by a new instance.equals() correctly. If the key is a String, verify whitespace or case differences.Key takeaways
get()Common mistakes to avoid
4 patternsUsing map.get(key) != null as a null check when the map may contain null values
get() separately (or redesign to avoid null values).Checking containsKey() then immediately calling get() separately — two lookups instead of one
Calling containsKey(null) on a TreeMap or ConcurrentHashMap
Assuming containsKey() is the same as get() != null in unit tests that never use null values
get() != null patterns.Interview Questions on This Topic
What is the difference between map.containsKey(k) and map.get(k) != null?
get() != null is not a safe key existence check when null values are possible. Use containsKey() if you need to know whether the key is present. Also note that getOrDefault() handles the fallback for absent keys in one call, but still returns the default for null-valued keys.Frequently Asked Questions
That's Collections. Mark it forged?
4 min read · try the examples if you haven't