10

I have map Map sampleMap ;

I have little confusion on the below two map methods. i got a review saying that to use containsKey method than get method. But i am thinking in a way that the get will also check the value is null or not ?

if(sampleMap.get('XXX')  != null )
{
   //do stuff
}

if(sampleMap.containsKey('XXX'))
{
   //do stuff
}

Suggest me !

4 Answers 4

27

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 Keys and Sets.

In practice, I've found that the majority of the time, you'll need to use the results that you get back from the map, so it is important that you cache the results of get so that you're not using containsKey or get more frequently than necessary. In other words, the optimal design choice usually look like this:

// Replace "Object" with the appropriate data type
Object value = someMap.get(key);
if(value != null) {
    // Do something with value
}

This pattern means you won't need to call both containsKey and get, which reduces CPU time, sometimes significantly. As I stated above, make sure that, if you're using custom types for keys, that non-equal values return unique hashCode values.

For example, this is a bad key design:

public class Key {
    public Integer hashCode() {
        return 1;
    }
    public Boolean equals(Object o) {
        return o === this;
    }
}

This is because the number of equals methods that will be called is up to the number of elements in the set or map key set, which can easily be hundreds of times more costly than an optimal key design:

public class Key {
    static Integer counter = 0;
    Integer code;
    public Key() {
        code = counter++;
    }
    public Integer hashCode() {
        return code;
    }
    public Boolean equals(Object o) {
        return o === this;
    }
}

By returning unique hashCode values for values which are not equal, you'll drastically improve the performance of both get and containsKey.

Keep in mind that most of the time, you don't care if a map contains a particular key, you care about the value being returned having a null value (to avoid NullPointerException). It is incredibly rare that you'll ever only care about the presence of a key in a map (containsKey), and it is extremely common that you'll care about NullPointerException.

So, in conclusion, I would say that the most efficient design pattern will almost always be to cache the results of get, and not use containsKey. containsKey is almost always just a CPU sink, wasting precious CPU cycles when you're just going to be calling get anyways. It takes approximately the same amount of time to call get and containsKey, and it's virtually guaranteed that after you call containsKey, you're going to call get anyways, so you may as well cut out the middle man.

If you write code that ever only uses containsKey and doesn't use get, you should be using a Set, not a Map. If you're using containsKey and get, you're probably using a sub-optimal algorithm. You should call get no more than once per key for optimal performance.

The rare case for using containsKey used to be when initializing lists or sets in maps, like this:

Map<Id, Contact[]> contacts = new Map<Id, Contact[]>();
for(Contact record: [SELECT AccountId FROM Contact]) {
    if(contacts.containsKey(record.AccountId)) {
        contacts.get(record.AccountId).add(record);
    } else {
        contacts.put(record.AccountId, new List<Contact> { record });
    }
}

However, in the past year or so, I've designed an even more optimal design:

Contact[] temp;
Map<Id, Contact[]> contacts = new map<Id, Contact[]>();
for(Contact record: [SELECT AccountId FROM Contact]) {
    if((temp = contacts.get(record.AccountId)) == null) {
        contacts.put(record.AccountId, temp = new Contact[0]);
    }
    temp.add(record);
}

While you'll want to add comments to explain this, this solution combines a null check with caching to produce optimal CPU usage. When you want to aggregate data together this way, this combined caching strategy outperforms any other design I've written seen or used by a large margin, particularly when you need the performance boost in triggers.

1
  • 6
    very slick setting the var value in the IF Commented Feb 5, 2020 at 22:16
19

Don't get hung up on efficiency: clear code is always the first priority. If containsKey is clearer use that. If you want to use the value from the map (so it mustn't be null) use the get with a null check.

This provides an example of a null map value where the methods give different results:

Map<String, Object> m = new Map<String, Object>{'abc' => null};
// This outputs false
System.debug(m.get('abc') != null);
// This outputs true
System.debug(m.containsKey('abc'));
0
6

As per the documentation

containsKey(key)

Returns true if the map contains a mapping for the specified key.

And get(key)

Returns the value to which the specified key is mapped, or null if the map contains no value for this key.

So I think using get(key) will make more sense, since you again don't have to add null for that value as well but use what is more Readable and understandable.

Below are different scenarios with different outputs so I think it makes sense to use containsKey() first and then add null check.For example:

Map<Integer,String> myMap = new Map<Integer,String>();
myMap.put(1,'One');
myMap.put(2,'Two');
myMap.put(3,'Three');
myMap.put(4,null);

if(myMap.containsKey(4))
    System.debug('4 containsKey True'); //Prints True
else
    System.debug('4 containsKey False');
if(myMap.get(4) != null)
    System.debug('4 Get True');
else
    System.debug('4 Get False');    //Prints False
if(myMap.get(5) != null)
    System.debug('5 get True');
else
    System.debug('5 get False');    //Prints False
2

Had to throw my three-line solution into the ring:

Map<Id, Id[]> AccountsAndOpportunitiesMap = new Map<Id, Id[]>();
for (Opportunity o : [SELECT Id, AccountId FROM Opportunity) {
    Id[] oppIds = AccountsAndOpportunitiesMap.containsKey(o.AccountId) ? AccountsAndOpportunitiesMap.get(o.AccountId) : new Id[]{};
    oppIds.add(o.Id);
    AccountsAndOpportunitiesMap.put(o.AccountId, oppIds);                     
}

Curious how it stacks up, performance wise.

3
  • 1
    Just wandered over here because of another Q. To answer your "question", this requires 2 hashCode if the key does not exist, 3 if it does, while my solution requires 2 if the key does not exist, and 1 if it does; this means that this solution will never be more efficient than mine, and will perform worse with low cardinality (e.g. many records that match a few keys).
    – sfdcfox
    Commented Nov 10, 2021 at 15:37
  • Real talk - because I've been doing it this way for the last two years - what are the real world implications here? Are we talking about adding a few ms to my execution time or is there a significant impact on performance?
    – Isaac L
    Commented Nov 11, 2021 at 17:33
  • 1
    It's circumstantial. Might be a few ms, might be a few thousand ms. It's okay in a few places, but will likely nickle-and-dime you into CPU limits if used extensively, with large data sets/low key cardinality, when debugging at FINER or FINEST, or with keys with poor hashCode/equals performance. I'd say, if you're satisfied with your code's performance, don't worry about it, but if you have been running into issues, it's at least worth profiling your maps to if it's a problem. A more comprehensive answer would need a full Q&A.
    – sfdcfox
    Commented Nov 11, 2021 at 18:15

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .