Can sObjects Be Used as Keys in Maps?

Question:
Are sObjects supported as keys in Apex Maps? While using sObjects as Map keys, any subsequent changes to the key sObject seem to break the mapping between the key and the corresponding value. Is this the expected behavior, and if so, how can it be handled?
Here’s an example demonstrating the issue:
Map<OpportunityLineItem, String> testMap = new Map<OpportunityLineItem, String>();
OpportunityLineItem testOli = new OpportunityLineItem();
testOli.Description = 'foo';
testMap.put(testOli, 'bar');
System.assert(testMap.containsKey(testOli)); // Should pass
String mapValue = testMap.get(testOli);
System.assertEquals('bar', mapValue); // Should pass
// Modify the key sObject
testOli.OpportunityId = '0064000000RB2eJ';
// The modified key no longer maps to the original value
System.assert(testMap.containsKey(testOli), 'Expected to still contain OLI'); // Fails
String mapValue2 = testMap.get(testOli);
System.assertEquals('bar', mapValue2, 'Expected OLI instance to map to "bar"'); // FailsThe test fails with the error:System.AssertException: Assertion Failed: Expected to still contain OLI
CRS Info Solutions provides top-notch Salesforce online course with real-time projects, certification guidance, interview coaching, and a practical, job-ready approach.
Answer:
Yes, this behavior is expected. In Apex, when you use an sObject as a key in a Map, the hash code of the sObject determines its mapping. However, modifying any field on the sObject changes its hash code, effectively breaking the mapping. After a modification, the original sObject no longer serves as a valid key for retrieving the value from the Map.
To address this issue, you can use one of the following approaches:
1. Avoid Modifying the sObject Key:
The simplest approach is to ensure that you do not modify the sObject after adding it to the Map. This might not always be feasible, especially in dynamic scenarios.
2. Use a Wrapper Class:
A robust solution is to wrap the sObject in a custom container class. This ensures the hash code and equality logic are consistent, even if the sObject’s fields are modified.
Here’s an example:
public class SObjectWrapper {
public sObject record;
public SObjectWrapper(sObject record) {
this.record = record;
}
// Ensure equality and hashCode use object identity, not field values
public override Boolean equals(Object other) {
return this == other;
}
public override Integer hashCode() {
return System.identityHashCode(this);
}
}
// Example Usage
OpportunityLineItem testOli = new OpportunityLineItem(Description = 'foo');
SObjectWrapper oliWrapper = new SObjectWrapper(testOli);
Map<SObjectWrapper, String> testMap = new Map<SObjectWrapper, String>();
testMap.put(oliWrapper, 'bar');
// Modify the original sObject
testOli.OpportunityId = '0064000000RB2eJ';
// The mapping still works because the wrapper is unaffected by changes to the sObject
System.assert(testMap.containsKey(oliWrapper)); // Passes
System.assertEquals('bar', testMap.get(oliWrapper)); // PassesExplanation:
The code defines a SObjectWrapper class that wraps around an sObject, ensuring that the sObject’s identity is used for equality and hash code comparisons. This approach helps maintain the integrity of the Map key, even if the sObject is modified later.
- SObjectWrapper Class:
- The
SObjectWrapperclass is a simple wrapper for any sObject. - The
recordfield holds the sObject instance that is being wrapped. - The constructor takes an sObject as a parameter and assigns it to the
recordfield.
- The
public class SObjectWrapper {
public sObject record;
public SObjectWrapper(sObject record) {
this.record = record;
}
}- Override
equalsMethod:
- The
equalsmethod is overridden to compare the object reference (identity) rather than the field values. This ensures that twoSObjectWrapperinstances are considered equal only if they are the same object in memory, not if they contain the same data. this == otherchecks if both the currentSObjectWrapperobject and theotherobject reference the same instance in memory.
- The
public override Boolean equals(Object other) {
return this == other;
}- Override
hashCodeMethod:
- The
hashCodemethod is overridden to return a hash code based on the identity of theSObjectWrapperobject, ensuring that the Map uses the object reference for hashing. System.identityHashCode(this)returns the hash code associated with the object’s identity, which remains consistent even if the sObject is modified.
- The
public override Integer hashCode() {
return System.identityHashCode(this);
}
- Example Usage:
- A new
OpportunityLineItemobject (testOli) is created and initialized with aDescriptionfield value of'foo'. - An instance of
SObjectWrapper,oliWrapper, is created withtestOlipassed as an argument. - A Map (
testMap) is created withSObjectWrapperas the key type andStringas the value type. The Map stores theoliWrapperas the key and associates it with the value'bar'.
- A new
OpportunityLineItem testOli = new OpportunityLineItem(Description = 'foo');
SObjectWrapper oliWrapper = new SObjectWrapper(testOli);
Map<SObjectWrapper, String> testMap = new Map<SObjectWrapper, String>();
testMap.put(oliWrapper, 'bar');- Modify the Original sObject:
- The
OpportunityIdof thetestOlisObject is modified. However, this modification does not affect theoliWrapperbecause the wrapper class uses the identity hash code and equality comparison based on the object reference. - Since the
oliWrapperholds a reference to the originaltestOlisObject, modifyingtestOlidoes not change the identity ofoliWrapper, thus ensuring the key in the map remains valid.
- The
testOli.OpportunityId = '0064000000RB2eJ';- Check the Mapping:
- The
testMap.containsKey(oliWrapper)assertion passes, because the identity of theoliWrapperremains the same, even thoughtestOliwas modified. - The
testMap.get(oliWrapper)assertion returns'bar', the value originally mapped tooliWrapper, proving that the map still works correctly with the wrapper key.
- The
System.assert(testMap.containsKey(oliWrapper)); // Passes
System.assertEquals('bar', testMap.get(oliWrapper)); // PassesKey Points:
- The
SObjectWrapperclass ensures that modifications to the wrapped sObject (like changing its fields) do not affect the key used in the Map. - By overriding
equalsandhashCode, the Map’s integrity is preserved by using the object reference, not field values, for comparisons. - This solution addresses the issue where modifying an sObject can break its mapping in a Map, making it a more reliable method of using sObjects as Map keys in Apex.
3. Clone the sObject Key
Another approach is to clone the sObject before modifying it and use the clone as a new key if necessary. This ensures the integrity of the Map’s key-value relationship but can be cumbersome in practice.
OpportunityLineItem testOli = new OpportunityLineItem(Description = 'foo');
testMap.put(testOli, 'bar');
// Clone before modification
OpportunityLineItem clonedOli = testOli.clone();
clonedOli.OpportunityId = '0064000000RB2eJ';
// Use the cloned object for further operations
System.assert(!testMap.containsKey(clonedOli)); // The original key still works
System.assertEquals('bar', testMap.get(testOli)); // PassesHere’s an explanation of the code snippet provided:
OpportunityLineItem testOli = new OpportunityLineItem(Description = 'foo');
testMap.put(testOli, 'bar');- An instance of
OpportunityLineItemis created and initialized with theDescriptionfield set to'foo'. - This instance,
testOli, is used as a key in thetestMapwith the value'bar'. ThetestMapis aMap<OpportunityLineItem, String>, meaning thetestOliobject is being mapped to the string'bar'.
// Clone before modification
OpportunityLineItem clonedOli = testOli.clone();
clonedOli.OpportunityId = '0064000000RB2eJ';- A clone of the
testOliobject is created using theclone()method. Theclone()method creates a new instance that has the same field values as the original object (testOli), but it’s a separate object in memory. This cloned object is assigned to theclonedOlivariable. - The
OpportunityIdfield of theclonedOliobject is modified by setting it to'0064000000RB2eJ'. However, the originaltestOliobject remains unaffected by this change.
// Use the cloned object for further operations
System.assert(!testMap.containsKey(clonedOli)); // The original key still works
System.assertEquals('bar', testMap.get(testOli)); // Passes- The
containsKeymethod is called on thetestMapto check if the cloned object (clonedOli) is present as a key in the map. SinceclonedOliis a new instance (even though it has the same data astestOli), it does not exist as a key in thetestMap. The assertion!testMap.containsKey(clonedOli)passes, confirming that the cloned object is not in the map. - The
getmethod retrieves the value associated with the original keytestOlifrom the map. SincetestOliwas the original key and its value was'bar', the assertionSystem.assertEquals('bar', testMap.get(testOli))passes, confirming that the map still maps the originaltestOliobject to the value'bar'.
Key Points:
- The clone of
testOli(clonedOli) is a separate object in memory, even though it has the same field values. Therefore, the map does not recognizeclonedOlias the same key astestOli. - The
testMapstill contains the original key-value pair, mappingtestOlito'bar', becausetestOliitself has not been modified. - The clone (
clonedOli) can be modified independently of the originaltestOli, but modifying the clone doesn’t affect the original map since it’s a separate object with a different identity.
CRS Info Solutions offers industry-focused Salesforce training in Bangalore, tailored to help you achieve your career goals. Our courses provide real-world project experience, ensuring hands-on expertise while covering Admin, Developer, and AI modules. Salesforce training in Bangalore We also offer structured certification guidance, interview coaching, and detailed class notes to boost your confidence. Join our free demo class and take the first step toward a rewarding career in Salesforce.
Summing Up:
Using sObjects as Map keys can lead to unpredictable behavior if the sObject is modified. The recommended solution is to wrap the sObject in a custom container class to maintain consistency. Alternatively, avoid modifying the sObject after adding it to the Map or use cloning strategies to work around the issue.

