2

I like to know if this is a known issue in Salesforce Apex.

I created 2 identical sets of Objects, s1 and s2. s1 is used as a control test while s2 is the subject of a test to show the contains() method returns different values after the s2.toString() method is called. I am not able to find any error in the experiment below, hope someone is able to explain the observation. Thanks.

public class CustomClass {
    public String name { get; set; }
    public Integer age { get; set; }
    
    public CustomClass(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    public Boolean equals(Object other) {
        System.debug('In equals method');
        if (this === other)
        {
            return true;
        }
        CustomClass that = other instanceof CustomClass ? (CustomClass)other : null;
        System.debug('this: ' + this.toString());
        System.debug('that: ' + that.toString());

        System.debug('Equality: ' + (that != null && this.age == that.age && this.name == that.name));
        return that != null && this.age == that.age && this.name == that.name;
    }
    public Integer hashCode() {
        System.debug('In hashCode of: ' + this.name);
        Integer prime = 31;
        Integer result = 1;
        
        result = prime * result + ((name == null) ? 0 : System.hashCode(name));
        result = prime * result + ((age == null) ? 0 : System.hashCode(age));
        System.debug('HashCode is ' + result);
        return result;
    }
}

CustomClass alice25 = new CustomClass('Alice', 25);

//Control codes (copy and paste to avoid typo) to show s1.contains() is true
Set<Object> s1 = new Set<Object>{'Apple', 'Banana', 123, 456, new CustomClass('Alice', 25)};
System.assert(s1.contains(new CustomClass('Alice', 25)));

// test codes
Set<Object> s2 = new Set<Object>{'Apple', 'Banana', 123, 456, new CustomClass('Alice', 25)};
System.debug(s2); // this line caused the contains() method to return false
System.assert(s2.contains(new CustomClass('Alice', 25))); // true if comment out the toString above, false if doing a s2.toString()

Edit: Doing a System.debug(s2) alone is enough to cause this inconsistency. I understand the System.debug somewhat invokes the s2.toString(). Simplified the codes above to focus on the key issues.

2
  • I know there are some strange behaviours around toString (implicitly with System.debug) and Map, and that Map and Set are implementationally linked. With Map, the issue relates to mutable values being used as keys. Mutating a key causes contains to fail, but once toString is invoked the internal structure of the Map gets corrected and contains works again. I realise this is different to your scenario, which does not appear to include any mutation, but I suspect they are somehow related.
    – Phil W
    Commented Apr 23, 2023 at 14:28
  • @PhilW Your suspicion appears accurate, based on some testing I did for this question. The cleaning algorithm that's used in FINEST or when calling toString, along with an apparent bug in how <Object> collections are implemented (and possibly other base classes), seems to be the culprit here.
    – sfdcfox
    Commented Apr 24, 2023 at 1:50

1 Answer 1

3

There are two main effects that seem to be working against you.

First, is that there is an optimization that is triggered under certain circumstances. The two I know of are setting a TraceFlag.ApexCode to FINEST, and the second is using Object.toString() in any manner on a Map or Set. When these conditions are met, a different algorithm is used to locate items in the collection, which triggers this bug. Try setting the trace flag, as mentioned earlier, and your code should now assert fail on line 38 instead of 43.

Second, there appears to be a bug/optimization that occurs based on the data type that you choose for the data type. If you set logging to FINEST, and you run your code, you'll notice this:

SYSTEM_METHOD_ENTRY|[38]|Set<ANY>.contains(Object)
SYSTEM_METHOD_EXIT|[38]|Set<ANY>.contains(Object)
SYSTEM_METHOD_ENTRY|[38]|System.assert(Boolean)
EXCEPTION_THROWN|[38]|System.AssertException: Assertion Failed
HEAP_ALLOCATE|[38]|Bytes:20
SYSTEM_METHOD_EXIT|[38]|System.assert(Boolean)
FATAL_ERROR|System.AssertException: Assertion Failed

Do you notice anything weird? Your hashCode and equals methods are not being called at all! In other words, you're using a default implementation, and that is likely based on Object.equals and Object.hashCode, which are based on memory references and other weird mechanics. In other words:

CustomClass c1 = new CustomClass('Alice',25), c2 = new CustomClass('Alice',25);
Assert.isTrue(c1 === c2); // fails!

Because === is used instead of ==, your equals method never gets a chance to run.

So, the first possible fix is to not create new values every time:

CustomClass alice25 = new CustomClass('Alice', 25);
Set<Object> s1 = new Set<Object>{'Apple', 'Banana', 123, 456, alice25};
// Will always work as expected
System.assert(s1.contains(alice25));

The alternative would be to make sure that your custom implementations are called every time by using the CustomClass:

Set<CustomClass> s1 = new Set<CustomClass>{new CustomClass('Alice', 25), new CustomClass('Bob', '24')};
Assert.isTrue(s1.contains(new CustomClass('Alice', 25)));

I don't think this is a bug that will ever be fixed, so you should be aware of these mechanics. You should read Using Custom Types in Map Keys and Sets. If you strictly follow the example code and the associated warnings, you should probably be fine. For example, you could create an Object2 that holds an Object, and does the appropriate hashCode and equals, and that should work in all cases. Similarly, I wouldn't trust interfaces, virtual classes, or abstract classes to work as a custom type, because the documentation does not say that this is supported.

You must log in to answer this question.

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