7

This is probably best summed up with an example and quote from the documentation.

The following anonymous Apex will pass the given assertion.

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 Set of Objects 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.

How can I work around this Set limitation when changing a field value?

2 Answers 2

5

This interesting Apex oddity came up when I was debugging a problem around hash code collisions.

Just by adding a System.debug statement that drops the contents of the Set it becomes possible to find the record in the Set again.

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));

// Here be dragons. Thou art forewarned
System.debug(accountSet);
System.assert(accountSet.contains(a2));

If that isn't enough to get you scratching your head. Changing the Apex Log level to none does not affect the outcome. The same if the line is changed to System.debug(LoggingLevel.FINEST, accountSet); and the Apex code log level set to Error.

Note that I haven't tested this with large sets where the complete contents isn't iterated.

Your milage may vary - best not to depend on this behavior for anything important. For instance, @LanceShi reported that it wasn't working in his orgs. I found that this was the case when the Apex Log level was set to Finest.

12
  • 1
    Dragons indeed.
    – Adrian Larson
    Jan 10, 2017 at 22:46
  • 1
    +1 but remember that debugging objects takes CPU time, and takes correspondingly longer the larger the object, even if it is not "fully iterated", and even if the results are not logged. Just did some benchmarking. It's not a deal-breaker to use once, but definitely avoid it in a loop.
    – sfdcfox
    Jan 10, 2017 at 22:48
  • I kinda get why they say remove all debug from a managed package. It still appears to work if I use System.debug(LoggingLevel.FINEST, accountSet); and set the Apex Code log level to Error. It is still being evaluated and having a side effect even when it doesn't appear in the log. Jan 10, 2017 at 22:59
  • 1
    Non-primitive data types are added to collections by reference, so when they say won’t be found anymore when using contains just means contains validation isn't working on updated records until collection is being referenced.
    – o-lexi
    Jan 10, 2017 at 23:32
  • 2
    @DanielBallinger This code doesn't always work. I just tested that and the test case failed for me.
    – Lance Shi
    Jan 11, 2017 at 0:02
0

In addition to Daniel's answer and experiment, I did a bit more investigation into this, which can be described by the following code:

@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));

    }
}

DEBUG LOG:

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

Based on current experiment:

  1. Hashcode of sobject changes after you changed a field value - actually you changed into a minus value
  2. Hashcode of a custom object doesn't change after you changed a field value.
  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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .