How does contains work for a set of objects

The question

In Apex, we all know that set can’t have duplicate values. And a set can tell whether it contains a certain element or not. However, things got complicated when the set is composed of objects (or SObjects). When you change a field of the object record, the set won’t recognize the same record any more. It just refuse to think it contains such an element.

To eleborate, the following test code will pass:

Account a1 = new Account(name='a');
Account a2 = new Account(name='b');
Set<Account> accountSet = new Set<Account>{a1, a2};
System.assertEquals(accountSet.size(), 2);
System.assert(accountSet.contains(a2));
a2.description = 'foo';
System.assert(!accountSet.contains(a2));

This lines up with the salesforce documentation:

If set elements are objects, and these objects change after being added to the collection, they won’t be found anymore when using, for example, the contains or containsAll methods, because of changed field values.

This, brings up one question: how does contains work in Apex?

Further, Daniel Ballinger has examined that if the debug log level is set to be debug. If we put a System.debug(accountSet) right ahead of the last System.assert. accountSet.contains(a2) will actually become true! How is that possible?

The experiment

After Daniel’s experiment on this. I did some further experiments. The last test class looks like below:


@isTest
public class TestSetObject 
{
    @isTest
    public static void testAccountSet()
    {
        Account a1 = new Account(name='a');
        Account a2 = new Account(name='b');
        System.debug('a2 hashcode is ' + System.hashCode(a2));
        Set<Account> accountSet = new Set<Account>{a1, a2};
        System.assertEquals(accountSet.size(), 2);
        System.assert(accountSet.contains(a2));
        a2.description = 'foo';
        System.assert(!accountSet.contains(a2));
        System.debug('a2 hashcode is ' + System.hashCode(a2));

        for(Account a: accountSet) 
        {
            System.debug('current account is: ' + a);
            System.debug('current account hashcode is: ' + System.hashCode(a));
        }

        System.debug(accountSet.contains(a2));

        // Here be dragons. Thou art forewarned
        System.debug(accountSet);
        for(Account a: accountSet) 
        {
            System.debug('current account is: ' + a);
            System.debug('current account hashcode is: ' + System.hashCode(a));
        }
        System.assert(accountSet.contains(a2));
    }

    @isTest
    public static void testObjectSet()
    {
        TestObject to1 = new TestObject();
        to1.name = 'test1';
        TestObject to2 = new TestObject();
        to2.name = 'test2';

        System.debug('to2 hashcode is:' + to2.hashcode());

        Set<TestObject> testSet = new Set<TestObject>{to1, to2};
        to2.name = 'Hello';
        to2.description = 'Sample';
        System.debug('to2 hashcode is:' + to2.hashcode());

        System.assert(testSet.contains(to2));

    }
}

The result

The debug log of the previous code looks like below:

11:47:22:002 USER_DEBUG [9]|DEBUG|a2 hashcode is 2420425

11:47:22:002 USER_DEBUG [15]|DEBUG|a2 hashcode is -54290973

11:47:22:002 USER_DEBUG [19]|DEBUG|current account is: Account:{Name=a}

11:47:22:002 USER_DEBUG [20]|DEBUG|current account hashcode is: 2420426

11:47:22:002 USER_DEBUG [19]|DEBUG|current account is: Account:{Name=b, Description=foo}

11:47:22:002 USER_DEBUG [20]|DEBUG|current account hashcode is: -54290973

11:47:22:003 USER_DEBUG [23]|DEBUG|false

11:47:22:003 USER_DEBUG [26]|DEBUG|{Account:{Name=a}, Account:{Name=b, Description=foo}}

11:47:22:003 USER_DEBUG [29]|DEBUG|current account is: Account:{Name=a}

11:47:22:003 USER_DEBUG [30]|DEBUG|current account hashcode is: 2420426

11:47:22:003 USER_DEBUG [29]|DEBUG|current account is: Account:{Name=b, Description=foo}

11:47:22:003 USER_DEBUG [30]|DEBUG|current account hashcode is: -54290973

11:47:22:057 USER_DEBUG [43]|DEBUG|to2 hashcode is:474756083

11:47:22:057 USER_DEBUG [48]|DEBUG|to2 hashcode is:474756083

Conclusion

Contains, like other Salesforce object equals functionality, is first based on hashcode values. There are certain stuffs I still not quite sure about. But here are the conclusions we can get from the above experiment:

  1. Hashcode of sobject changes after you changed a field value – actually it changed into a negative value
  2. Hashcode of a custom object seems don’t change after you changed a field value. But don’t rely on this. You should still override hashcode and equals method if you want to use set of custom objects. The document is here.
  3. The hashcode of the elements in the set will update immediately after the update of set element’s value.
  4. I am not 100% sure how contains work in Set. But based on current experiment, it is probably a separate data structure (binary tree?) contains the existing hashcode values.
  5. If the debug log level is set to be DEBUG, after system.debug, the data structure will probably get refreshed.

Next post

Arithmetic shift and logical shift in Apex

Subscribe to Sfdcinpractice

Subscribe to get the latest blogs and tutorials of sfdcinpractice. No spam, no trash, only the awesome posts from sfdcinpractice. 

Comments

  1. Anujit - January 16, 2017 @ 8:05 am

    very good information. thanks for the post. keep up the good work!

Leave a Reply

Your email address will not be published / Required fields are marked *