Is it more efficient to use map in apex & call .get(), or use a set and call.contains()?
Prelude
I did a couple of interwebs searches, and I used the following search terms here on SFSE...
apex map get set contains
apex map vs set
apex map set efficient*
...then looked through a number of Qs & As and some links within some of the As. If I missed the answer, please forgive and point me to it.
Background
I know just enough of Apex and other coding to be dangerous. Adhering to best practices as much as possible is preferable, but I don't know all the best practices. I also know that some debates are akin to "Toilet paper over or under?", in that there are lots of opinions but not often is there a real better answer. (There is a correct answer to the toilet paper question, btw.)
Item at hand
I was sent an Apex class with an invocable method which a different team had created and our team needs to use. One bit of functionality that it was supposed to be doing was not working as stated, and some parts seemed to me that they could use some improvement. So I modified the code, and it was sent back to the code owners for their consideration. They kept most of my changes, but a blanket statement was made that I want to know whether it is true. Not (necessarily) so that I can say that I am correct (if I am), but just to know as part of the aforementioned best practices.
Statement
Maps are more efficient [than Sets] when doing a lookup.
Question
Is it more efficient to use a map and call .get(), or use a set and call .contains()?
Code Comparison
Showing only the pertinent code (and changing names to protect the [not so] innocent), here is the original:
Listgroups = [SELECT Id, Name FROM Group WHERE Name LIKE :queryString]; MapgroupMap = new Map ();
for (Group g: groups) {
groupMap.put(g.Name, g);
}
Group targetGroup;
String targetGroupName;
for (...) {
targetGroupName =targetGroup = groupMap.get(targetGroupName);
if(targetGroup != null){
// Found the only one we're looking for, so set the output
break;
}
}
My suggested changes:
Listgroups = [SELECT Name FROM Group WHERE Name LIKE :queryString]; SetgroupNameSet = new Set ();
for (Group g: groups) {
groupNameSet.add(g.Name);
}
String targetGroupName;
for (...) {
targetGroupName =if (groupNameSet.contains(targetGroupName))
// Found the only one we're looking for, so set the output
break;
}
}
I ran some benchmark tests based on your two examples. I assumed going into it the difference was going to be slight and the results seem to reflect that. Based on your use case, I don't see a reason to build a map at all since you're most interested in whether the keySet of the map itself contains a value (related, you can call containsKey on a map much like contains on a set). Your most likely performance hit will be in storing all of your map values in memory and not necessarily CPU time in accessing your data structure.
A few notes on my test: I'm using a traditional for-loop so that I can traverse the data set 100,000 times. There are performance differences between this loop syntax and the map in Apex for-in loop syntax. I'm using a map with 100 mock values in it. The size of the map or set shouldn't be a factor in performance where CPU is concerned, only in heap being used. The if statements are copied from your sample code, only I'm never interested in the loop actually exiting early so the conditions are the inverse so that the loop runs all the way through. Using Map.get() shouldn't be a factor in performance where CPU is concerned, only in heap being used. The if statements are copied from your sample code, only I'm never interested in the loop actually exiting early so the conditions are the inverse so that the loop runs all the way through.
Using Map.get()
Map mockMap = new Map{
'Land iguana' => 'Conolophus subcristatus',
'Goliath heron' => 'Ardea goliath',
'Pelican, great white' => 'Pelicans onocrotalus',
'White-faced whistling duck' => 'Dendrocygna viduata',
'Bottle-nose dolphin' => 'Tursiops truncatus',
'Bee-eater, nubian' => 'Merops nubicus',
'Duck, white-faced whistling' => 'Dendrocygna viduata',
'Galapagos hawk' => 'Buteo galapagoensis',
'Egyptian cobra' => 'Naja haje',
'Snake, carpet' => 'Morelia spilota variegata',
'Feral rock pigeon' => 'Columba livia',
'Red-capped cardinal' => 'Paroaria gularis',
'Eastern boa constrictor' => 'Acrantophis madagascariensis',
'Eastern grey kangaroo' => 'Macropus giganteus',
'Bear, grizzly' => 'Ursus arctos',
'Duck, white-faced whistling' => 'Dendrocygna viduata',
'Masked booby' => 'Sula dactylatra',
'Squirrel, grey-footed' => 'Paraxerus cepapi',
'Mississippi alligator' => 'Alligator mississippiensis',
'Southern hairy-nosed wombat' => 'Lasiorhinus latifrons',
'Squirrel, eastern fox' => 'Sciurus niger',
'Malleefowl' => 'Leipoa ocellata',
'Heron, giant' => 'Ardea goliath',
'Great white pelican' => 'Pelecans onocratalus',
'Ox, musk' => 'Ovibos moschatus',
'Dusky gull' => 'Larus fuliginosus',
'Black-throated butcherbird' => 'Cracticus nigrogularis',
'African buffalo' => 'Syncerus caffer',
'Palm squirrel' => 'Funambulus pennanti',
'Lizard, frilled' => 'Chlamydosaurus kingii',
'Brown capuchin' => 'Cebus apella',
'Monitor, two-banded' => 'Varanus salvator',
'American woodcock' => 'Scolopax minor',
'White-winged dove' => 'Zenaida asiatica',
'Guanaco' => 'Lama guanicoe',
'Crowned eagle' => 'Spizaetus coronatus',
'Long-nosed bandicoot' => 'Perameles nasuta',
'Adouri (unidentified)' => 'unavailable',
'Lesser flamingo' => 'Phoeniconaias minor',
'Wagtail, african pied' => 'Motacilla aguimp',
'Boa, malagasy ground' => 'Acrantophis madagascariensis',
'Red and blue macaw' => 'Ara chloroptera',
'Barbet, crested' => 'Trachyphonus vaillantii',
'Buffalo, american' => 'Bison bison',
'Siskin, pine' => 'Carduelis pinus',
'Boa, columbian rainbow' => 'Epicrates cenchria maurus',
'Cat, kaffir' => 'Felis silvestris lybica',
'Monitor, two-banded' => 'Varanus salvator',
'Swan, trumpeter' => 'Cygnus buccinator',
'Tsessebe' => 'Damaliscus lunatus',
'Mocking cliff chat' => 'Thamnolia cinnmomeiventris',
'European shelduck' => 'Tadorna tadorna',
'White-tailed jackrabbit' => 'Lepus townsendii',
'Marmot, yellow-bellied' => 'Marmota flaviventris',
'Crab (unidentified)' => 'unavailable',
'Duiker, common' => 'Sylvicapra grimmia',
'Hyena, spotted' => 'Crocuta crocuta',
'Pelican, australian' => 'Pelecanus conspicillatus',
'Bird, magnificent frigate' => 'Fregata magnificens',
'Mexican wolf' => 'Canis lupus baileyi',
'Emu' => 'Dromaius novaehollandiae',
'Barking gecko' => 'Phyllurus milli',
'Eastern indigo snake' => 'Drymarchon couperi',
'Salmon, sockeye' => 'Oncorhynchus nerka',
'White-fronted bee-eater' => 'Merops bullockoides',
'Antelope, roan' => 'Hippotragus equinus',
'Bent-toed gecko' => 'Cyrtodactylus louisiadensis',
'Sable antelope' => 'Hippotragus niger',
'Tammar wallaby' => 'Macropus eugenii',
'American badger' => 'Taxidea taxus',
'Glider, feathertail' => 'Acrobates pygmaeus',
'Weeper capuchin' => 'Cebus nigrovittatus',
'Civet, small-toothed palm' => 'Arctogalidia trivirgata',
'Coqui partridge' => 'Francolinus coqui',
'White-lipped peccary' => 'Tayassu pecari',
'Red-billed toucan' => 'Ramphastos tucanus',
'Pigeon, wood' => 'Columba palumbus',
'Cockatoo, red-breasted' => 'Eolophus roseicapillus',
'Peacock, indian' => 'Pavo cristatus',
'Carpet python' => 'Morelia spilota variegata',
'Monkey, black spider' => 'Ateles paniscus',
'Black-backed jackal' => 'Canis mesomelas',
'Lesser mouse lemur' => 'Microcebus murinus',
'Crested barbet' => 'Trachyphonus vaillantii',
'North American red fox' => ' vulpes',
'Snake (unidentified)' => 'unavailable',
'Jackal, silver-backed' => 'Canis mesomelas',
'Wolf, timber' => 'Canis lupus lycaon',
'Chuckwalla' => 'Sauromalus obesus',
'Sugar glider' => 'Petaurus breviceps',
'Heron, green-backed' => 'Butorides striatus',
'Blackbird, red-winged' => 'Agelaius phoeniceus',
'Red meerkat' => 'Cynictis penicillata',
'Wood pigeon' => 'Columba palumbus',
'Starling, greater blue-eared' => 'Lamprotornis chalybaeus',
'Giant otter' => 'Pteronura brasiliensis',
'Suricate' => 'Suricata suricatta',
'Flicker, field' => 'Colaptes campestris',
'Red-cheeked cordon bleu' => 'Uraeginthus bengalus'
};
List commonNames = new List(mockMap.keySet());
Integer commonNamesSize = commonNames.size();
String targetName;
String name;
Integer startTime = Limits.getCpuTime();
for(Integer i = 0; i < 100000 xss=removed xss=removed xss=removed time = ' + String.valueOf(Limits.getCpuTime() - startTime));
Using Set.contains()
// Map instantiation code left out for brevity, see the code block above.
List commonNames = new List(mockMap.keySet());
Set nameSet = mockMap.keySet();
Integer commonNamesSize = commonNames.size();
String targetName;
String name;
Integer startTime = Limits.getCpuTime();
for(Integer i = 0; i < 100000; ++i) {
name = commonNames[Math.mod(i, commonNamesSize)];
if(!nameSet.contains(name)) {
break;
}
}
System.debug(' xss=removed>