A common pattern with a map is to check if a key exists and then act on the value only if it does, consider:
if(!map.containsKey(key)) {
map.put(key, new DefaultValue());
}
return map.get(key);
However this is generally considered poor, as it requires two map lookups, where this alternative only requires one:
Value result = map.get(key);
if(result == null)
{
result = new DefaultValue();
map.put(key,result);
}
return result;
Yet this second implementation has it's own problems. In addition to being less concise and readable, it's potentially incorrect, as it cannot differentiate between the case where the key does not exist and where the key exists but maps explicitly to null
. In individual cases of course we can create external invariants that the map will not contain null
values, however in general we cannot rely on this second pattern, needing to fall back to the less efficient implementation.
But why does the first implementation need to be less efficient? HashMap
's .containsKey()
looks like this:
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
And Guava's ImmutableMap.containsKey()
similarly is:
public boolean containsKey(@Nullable Object key) {
return get(key) != null;
}
Since these calls go through all the work of doing a .get()
, what is the disadvantage of caching the result of this call, and then short-circuting a successive call to .get()
for the same key? It seems like the cost is a single pointer, yet the benefit would mean the "right" way to implement such a pattern is also the "efficient" way to do so.
private transient Entry<K,V> lastContainsKeyResult = null;
public boolean containsKey(Object key) {
lastContainsKeyResult = getEntry(key);
return lastContainsKeyResult != null;
}
public V get(Object key) {
if(key != null && lastContainsKeyResult != null &&
key.equals(lastContainsKeyResult.getKey()) {
return lastContainsKeyResult.getValue();
}
// normal hash lookup
}
containsKey and get have the same performance characteristics. The majority of the CPU time used will be within the hashCode and equals methods for the objects used as keys. Poorly designed keys will have poor performance characteristics, particularly when you're using Custom Types in map methods in salesforce.
get(Object) does explicitly warn about the subtle differences between Map. get(Object) and Map. containsKey(Object) : If this map permits null values, then a return value of null does not necessarily indicate that the map contains no mapping for the key; it's also possible that the map explicitly maps the key to null .
Map. containsKey() method is used to check whether a particular key is being mapped into the Map or not. It takes the key element as a parameter and returns True if that element is mapped in the map.
Return Value: The method returns boolean true if the presence of the key is detected else false .
Because caching assumes a particular use-case but will actually slow things down in others. It also adds a lot of complications.
How do you cache the value? What happens when multiple threads are reading at once?
Sit down and start thinking through all the various edge cases and problems that can happen here. For example if the value gets changed between the contains call and the get call. Such a seemingly simple change actually introduced a lot of complexity and slows down a lot of operations which are actually more likely to be more frequently used than this specific sequence.
You should also consider that it's possible to build a "caching map" on top of a non-caching one but the opposite would not be possible.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With