Can sObjects Be Used as Keys in Maps?

Can sObjects Be Used as Keys in Maps?

On January 20, 2025, Posted by , In Salesforce Technical Questions, With Comments Off on Can sObjects Be Used as Keys in Maps?
Salesforce Technical Questions with Answers Apex, LWC, Marketing Cloud

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"'); // Fails

The 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)); // Passes

Explanation:

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.

  1. SObjectWrapper Class:
    • The SObjectWrapper class is a simple wrapper for any sObject.
    • The record field holds the sObject instance that is being wrapped.
    • The constructor takes an sObject as a parameter and assigns it to the record field.
public class SObjectWrapper {
    public sObject record;

    public SObjectWrapper(sObject record) {
        this.record = record;
    }
}

  1. Override equals Method:
    • The equals method is overridden to compare the object reference (identity) rather than the field values. This ensures that two SObjectWrapper instances are considered equal only if they are the same object in memory, not if they contain the same data.
    • this == other checks if both the current SObjectWrapper object and the other object reference the same instance in memory.
public override Boolean equals(Object other) {
    return this == other;
}
  1. Override hashCode Method:
    • The hashCode method is overridden to return a hash code based on the identity of the SObjectWrapper object, 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.
public override Integer hashCode() {
    return System.identityHashCode(this);
}
  1. Example Usage:
    • A new OpportunityLineItem object (testOli) is created and initialized with a Description field value of 'foo'.
    • An instance of SObjectWrapper, oliWrapper, is created with testOli passed as an argument.
    • A Map (testMap) is created with SObjectWrapper as the key type and String as the value type. The Map stores the oliWrapper as the key and associates it with the value 'bar'.
OpportunityLineItem testOli = new OpportunityLineItem(Description = 'foo');
SObjectWrapper oliWrapper = new SObjectWrapper(testOli);

Map<SObjectWrapper, String> testMap = new Map<SObjectWrapper, String>();
testMap.put(oliWrapper, 'bar');
  1. Modify the Original sObject:
    • The OpportunityId of the testOli sObject is modified. However, this modification does not affect the oliWrapper because the wrapper class uses the identity hash code and equality comparison based on the object reference.
    • Since the oliWrapper holds a reference to the original testOli sObject, modifying testOli does not change the identity of oliWrapper, thus ensuring the key in the map remains valid.
testOli.OpportunityId = '0064000000RB2eJ';
  1. Check the Mapping:
    • The testMap.containsKey(oliWrapper) assertion passes, because the identity of the oliWrapper remains the same, even though testOli was modified.
    • The testMap.get(oliWrapper) assertion returns 'bar', the value originally mapped to oliWrapper, proving that the map still works correctly with the wrapper key.
System.assert(testMap.containsKey(oliWrapper)); // Passes
System.assertEquals('bar', testMap.get(oliWrapper)); // Passes

Key Points:

  • The SObjectWrapper class ensures that modifications to the wrapped sObject (like changing its fields) do not affect the key used in the Map.
  • By overriding equals and hashCode, 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)); // Passes

Here’s an explanation of the code snippet provided:

OpportunityLineItem testOli = new OpportunityLineItem(Description = 'foo');
testMap.put(testOli, 'bar');
  • An instance of OpportunityLineItem is created and initialized with the Description field set to 'foo'.
  • This instance, testOli, is used as a key in the testMap with the value 'bar'. The testMap is a Map<OpportunityLineItem, String>, meaning the testOli object is being mapped to the string 'bar'.
// Clone before modification
OpportunityLineItem clonedOli = testOli.clone();
clonedOli.OpportunityId = '0064000000RB2eJ';
  • A clone of the testOli object is created using the clone() method. The clone() 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 the clonedOli variable.
  • The OpportunityId field of the clonedOli object is modified by setting it to '0064000000RB2eJ'. However, the original testOli object 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 containsKey method is called on the testMap to check if the cloned object (clonedOli) is present as a key in the map. Since clonedOli is a new instance (even though it has the same data as testOli), it does not exist as a key in the testMap. The assertion !testMap.containsKey(clonedOli) passes, confirming that the cloned object is not in the map.
  • The get method retrieves the value associated with the original key testOli from the map. Since testOli was the original key and its value was 'bar', the assertion System.assertEquals('bar', testMap.get(testOli)) passes, confirming that the map still maps the original testOli object 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 recognize clonedOli as the same key as testOli.
  • The testMap still contains the original key-value pair, mapping testOli to 'bar', because testOli itself has not been modified.
  • The clone (clonedOli) can be modified independently of the original testOli, 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.


















Comments are closed.