4

When a set collection contains objects such as Opportunities, what is used to determine uniqueness when the same Opportunity record is added to the collection a second time? I'm guessing that the system is considering the entire object (a hash?) so that any changes in the field values of a record results in the collection considering it as a new Opportunity record rather than replacing the previous one.

If I run this code the final count on the setOpportunities collection reveals that my assumption is correct:

Map<Id, Opportunity> mapOpportunities = new Map<Id, Opportunity>();
mapOpportunities.putAll([SELECT Id, Name, Type FROM Opportunity LIMIT 2]);
system.debug('Count of records in mapOpportunities = ' + mapOpportunities.size()); //size equals 2

Set<Opportunity> setOpportunities = new Set<Opportunity>();
for(Opportunity objOpportunity : mapOpportunities.values())
{
    setOpportunities.add(objOpportunity);
}
system.debug('Count of records in setOpportunities = ' + setOpportunities.size()); //size equals 2

for(Opportunity objOpportunity : mapOpportunities.values())
{
    setOpportunities.add(objOpportunity);
}
system.debug('Display setOpportunities records after being added again: ' + setOpportunities); //displays 2 records
system.debug('Count of records in setOpportunities after being added again = ' + setOpportunities.size()); //size equals 2

for(Opportunity objOpportunity : mapOpportunities.values())
{
    //edit each opportunity and add it to the set again.
    objOpportunity.Name = objOpportunity.Name + ' - Updated'; 
    setOpportunities.add(objOpportunity);
}
system.debug('Count of records in setOpportunities after being updated and added = ' + setOpportunities.size()); //size equals 4

What is odd is what happens if I display the setOpportunities collection in the log. If I add:

system.debug('Display setOpportunities records after being updated and added: ' + setOpportunities); //displays 2 records (shouldn't this display 4 records??)

Only the two updated Opportunity records are displayed. This sent me in circles for a while because I was debugging an upsert issue by displaying the content of the set and it appeared as if there was only 2 records rather than the actual 4.

Am I crazy? Any thoughts on why it only displays the 2?

  • 1
    A closely related question: Using an sObject as a Map key. Although that was about sObjects as keys rather than values. – Daniel Ballinger Mar 19 '14 at 19:34
  • Well. Having replicated your code, I can confirm the behaviour! Looking into it now. – Simon Lawrence Mar 19 '14 at 19:45
  • 2
    Also, weirdly, if you "for" through the final set, and output the opportunities, you get four records, and all of them have the "- Updated" flag on the name. – Simon Lawrence Mar 19 '14 at 19:50
5

I think what you are seeing is something related to the internal implementation of String.valueOf(Object) when the object is a Set and the subsequent conversion to JSON for inclusion in the log.

String.valueOf(Object)

If I add the following towards the end of the anonymous Apex there are indeed 4 Opportunity records in the set.

//...
system.debug('Count of records in setOpportunities after being updated and added = ' + setOpportunities.size()); 
//size equals 4

system.debug('Display setOpportunities records after being updated and added: ' + setOpportunities); 
//displays 2 records (shouldn't this display 4 records??)

for(Opportunity objOpportunity : setOpportunities)
{
    System.debug(objOpportunity);
}

Debug log showing the 4 Opportunities in the Set.
enter image description here

Unfortunately you can't get the internal hashCode() for an sObject. See Expose the Object's hashCode() method in apex. You could potentially implement your own wrapper object with a custom hashCode() implementation that uses the Id when available.


A simpler example

Add one Opportunity to a Set twice. Note the asserts do pass, but are commented out to make the logging easier to read.

Set<Opportunity> setOpportunities = new Set<Opportunity>();

Opportunity opp = new Opportunity();
opp.Name = '1';

setOpportunities.add(opp);

//System.assertEquals(1, setOpportunities.size());

opp.Name = '2';

setOpportunities.add(opp);

//System.assertEquals(2, setOpportunities.size());

If you look at the log you can see the Opportunity is at 0x5239e2f9 in the heap memory and the Set has allocated two references to it.

Log showing memory references

Mock up of the end state.

Example of memory structure

| improve this answer | |
  • interesting. If you see my comment on the question though (and in your screen shot) all four objects when Stringed up have the "Updated" Opportunity name too... meaning I think the set might be blending the Opportunities a bit somehow. Any thoughts on that? – Simon Lawrence Mar 19 '14 at 20:35
  • 1
    I think the Set is picking up the the changes in the HashCode, and hence is adding the same Opportunities to the Set again. When this happens it has two references to each sObject. As the references are in fact to the same sObject in memory you see the updated values on each. – Daniel Ballinger Mar 19 '14 at 20:49
  • Yep, that all sounds good to me! (I think this whole question could become a chat/wiki topic incidentally..) - so ulitimately we are saying that the behaviour asked about is due to String.valueOf(obj) not understanding how to throw up the contents of a set with the same reference multiple times to what it thought (at the time of adding) were "different" sObjects due to their hashes? Well. Get's a vote off me! I would be keen to find out more this if it evolves though! – Simon Lawrence Mar 19 '14 at 20:59
  • 1
    I'm fairly certain the shrinking count can be attributed to String.valueOf(Set) and the internal conversion it does to the JSON representation. It's hard to prove much more without access to the implementation. – Daniel Ballinger Mar 19 '14 at 21:15
  • this is a brilliant exposition and now I see why I always use maps of IDs to Sobjects to maintain uniqueness <g> - never really trusting the Sobject uniqueness in a collection feature. – cropredy Mar 20 '14 at 2:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.