As a Salesforce developer, how many times have you had to make a Map of records where the key is a property of those records, and the values are lists of matching records? For example, a Map of Contact records where the keys are the last names or the Account IDs.
i.e. How often have you written something like this?
List<Contact> contacts = new List<Contact> {
new Contact(FirstName = 'John', LastName = 'Smith'),
new Contact(FirstName = 'Jane', LastName = 'Smith'),
new Contact(FirstName = 'John', LastName = 'Not-Smith')
};
Map<String, List<Contact>> lastNameToContacts = new Map<String, List<Contact>>();
for(Contact thisContact : contacts) {
List<Contact> thisList = lastNameToContacts.get(thisContact.LastName);
if(thisList == null) {
thisList = new List<Contact> { thisContact };
lastNameToContacts.put(thisContact.LastName, thisList);
} else {
thisList.add(thisContact);
}
}
Assert.areEqual(new List<Contact> { contacts[0], contacts[1] }, lastNameToContacts.get('Smith'));
Assert.areEqual(new List<Contact> { contacts[2] }, lastNameToContacts.get('Not-Smith'));
Too many times! It's perfectly serviceable code, but it doesn't have to be so long-winded! And it doesn't have to be created from scratch every time. What if there were a reusable data structure to do this?
Well, the general name for a Map where the values are lists is a MultiMap, and we can write one (even though we don't have the ability to write our own generics yet 😥). With a MultiMap, the above example becomes like this:
MultiMap multiMap = new MultiMap(List<Contact>.class, new FieldFromSObject(Contact.LastName));
List<Contact> contacts = new List<Contact> {
new Contact(FirstName = 'John', LastName = 'Smith'),
new Contact(FirstName = 'Jane', LastName = 'Smith'),
new Contact(FirstName = 'John', LastName = 'Not-Smith')
};
multiMap.putAll(contacts);
Assert.areEqual(new List<Contact> { contacts[0], contacts[1] }, multiMap.getWithKey('Smith'));
Assert.areEqual(new List<Contact> { contacts[2] }, multiMap.getWithKey('Not-Smith'));
FieldFromSObject is not defined in that code sample, but you can probably guess that it reads a field value from an SObject record. And it can be reused every time you make a MultiMap of SObjects. If all you ever wanted to do was store SObjects, you could use the super-awesome SObjectIndex that I wrote years ago.
But, sometimes we need to store plain old Apex objects that have properties. And MultiMap lets you do that. You can store DTOs, you can store Map<String, Object>
like you would get from deserializing JSON. You can even store primitives in it and segment them using a key function (we'll see that later).
There are two key (dad joke alert! Get it? Key? We're talking about maps?) insights to make the MultiMap work:
- You can treat objects as if they are functions and write Apex as if functions are first class objects. All you need to do is define an interface that demands the function call you want. This means that we can pass a "function" into the MultiMap, defining how it should read keys from its records. That's the
FieldFromSObject
part in the example above (Note: this is the strategy pattern). - You can pass Type instances around so that, even if the declared type is
List<Object>
, MultiMap can create a List<Contact>
or List<MyType>
internally, allowing you to safely cast it back to the specific type after retrieval from the MultiMap.
The MultiMap implementation is this:
public MultiMap(Type listType, MultiMap.KeyReader keyReader) {
theMap = new Map<Object, List<Object>>();
this.listType = listType;
this.keyReader = keyReader;
}
public MultiMap(MultiMap.KeyReader keyReader) {
this(List<Object>.class, keyReader);
}
public void putAll(List<Object> items) {
Integer itemsSize = items.size();
for (Integer i = 0; i < itemsSize; i++) {
put(items[i]);
}
}
public void put(Object item) {
Object keyValue = keyReader.getKey(item);
List<Object> thisList = theMap.get(keyValue);
if (thisList == null) {
thisList = (List<Object>) listType.newInstance();
thisList.add(item);
theMap.put(keyValue, thisList);
} else {
thisList.add(item);
}
}
public List<Object> getWithKey(Object key) {
return theMap.get(key);
}
public List<Object> getWithItem(Object item) {
return theMap.get(keyReader.getKey(item));
}
public Set<Object> keySet() {
return theMap.keySet();
}
public interface KeyReader {
Object getKey(Object item);
}
It's really not much more complicated than what you've already written a million times. It just benefits from those two insights.
I promised that we would see how you could use it to segment lists of primitives. So, let's split lists of Integers based on whether they are odd or even. To do this, we need to define MultiMap.KeyReader that determines whether an Integer is even or odd:
private class IsEvenKey implements MultiMap.KeyReader {
public Object getKey(Object item) {
return ((Integer) item & 1) == 0;
}
}
What's happening here is:
- Cast the item Object that we expect to be an Integer into an Integer.
- Do a bitwise AND operation with 1.
- If the original item was odd, then the result will be 1. If it was even, then the result is 0.
Can you tell that I used to work for a hardware company? Using that KeyReader, we can write a test to see how it partitions out the Integers:
@IsTest
static void twoItemsDifferentKeys() {
MultiMap multiMap = new MultiMap(new IsEvenKey());
multiMap.put(5);
multiMap.put(6);
Assert.areEqual(5, multiMap.getWithItem(5)[0]);
Assert.areEqual(5, multiMap.getWithKey(false)[0]);
Assert.areEqual(6, multiMap.getWithItem(6)[0]);
Assert.areEqual(6, multiMap.getWithKey(true)[0]);
Assert.areEqual(new Set<Object>{ false, true }, multiMap.keySet());
}
Here at Processity , we're doing cool stuff with Field History Tracking, Audit Trail, Observability, and Process Mining on Salesforce. And when we have little bits of engineering to share with the community, we love to do that.
You can get the full source code for MultiMap on GitHub as a Gist - it's so small that it doesn't even need a repo. I hope you found this interesting!