11

When SObjects are used as Map keys they are compared by field values, so for example:

Account a = new Account();
Account b = new Account();
System.debug(a == b); // Prints true

Map<SObject, Boolean> m = new Map<SObject, Boolean>();
m.put(a, true);
m.put(b, false);

System.debug(m.get(a)); // Prints false

What I want instead is for them to be stored as separate keys. I've managed to achieve this by creating a wrapper class and overriding equals() using the exact equality operator (===).

private class SObjectReferenceWrapper
{
    public SObject Record;
        
    public SObjectReferenceWrapper(SObject record)
    {
        this.Record = record;
    }
        
    public Boolean equals(Object obj) 
    {
        if (obj instanceof SObjectReferenceWrapper) 
        {
            SObjectReferenceWrapper o = (SObjectReferenceWrapper)obj;
            return (Record === o.Record);
        }
            
        return false;
    }
}

Usage:

SObjectReferenceWrapper o = new SObjectReferenceWrapper(new Account());
SObjectReferenceWrapper o2 = new SObjectReferenceWrapper(new Account());
System.debug(o == o2); // Prints false

Map<SObjectReferenceWrapper, Boolean> m = new Map<SObjectReferenceWrapper, Boolean>();
m.put(o, true);
m.put(o2, false);

System.debug(m.get(o)); // Prints true

Account a = new Account();
SObjectReferenceWrapper o = new SObjectReferenceWrapper(a);
SObjectReferenceWrapper o2 = new SObjectReferenceWrapper(a);
System.debug(o == o2); // Prints true

Map<SObjectReferenceWrapper, Boolean> m = new Map<SObjectReferenceWrapper, Boolean>();
m.put(o, true);
m.put(o2, false);

System.debug(m.get(o)); // Prints false

Unfortunately, relying on equals() alone causes put() calls to take an incredibly long time once the map has 10-15 keys in it, so I need to implement hashCode() to resolve this. This is where I've come unstuck.

When all of the field values are the same the build in hashCode() method returns the same value, so that's no use.

I did also consider using the memory addresses of the objects which are show in in debug logs under VARIABLE_ASSIGNMENT, however it doesn't look like these are exposed via. Apex.

Have I come to a dead end where the limitations of the platform make this impossible (I hate that word), or am I missing something really obvious?

tl;dr

I need to create a Map using in memory SObjects as keys. Each in memory SObject should be treated as a unique value if it occupies a different location in memory, even if they have the same field values. Is this possible?

4 Answers 4

11
public class Wrapper {
    Sobject record;

    public Integer hashCode() {
        return System.hashCode(record);
    }

    public Boolean equals(Object o) {
        return record === ((Wrapper)o).record;
    }

    Wrapper(SObject record) {
        this.record = record;
    }
}

This works by using ===, which is a memory compare function. Test code ensues:

@isTest
public class sfse46415 {
    public class Wrapper {
        Sobject record;

        public Integer hashCode() {
            return System.hashCode(record);
        }

        public Boolean equals(Object o) {
            return record === ((Wrapper)o).record;
        }

        Wrapper(SObject record) {
            this.record = record;
        }
    }


    public static testmethod void test() {
        Contact a1 = new Contact(LastName='Test'),
                b1 = new Contact(LastName='Test'),
                c1 = new Contact(LastName='Test 2');
        Wrapper a2 = new Wrapper(a1),
                b2 = new Wrapper(b1),
                c2 = new Wrapper(c1),
                a3 = new Wrapper(a1),
                b3 = new Wrapper(b1),
                c3 = new Wrapper(c1);
        Set<Wrapper> items = new Set<Wrapper>();
        items.add(a2);
        System.assertEquals(true , items.contains(a2));
        System.assertEquals(false, items.contains(b2));
        System.assertEquals(false, items.contains(c2));
        System.assertEquals(true , items.contains(a3));
        System.assertEquals(false, items.contains(b3));
        System.assertEquals(false, items.contains(c3));
        items.add(b2);
        System.assertEquals(2, items.size());
        System.assertEquals(true , items.contains(a2));
        System.assertEquals(true , items.contains(b2));
        System.assertEquals(false, items.contains(c2));
        System.assertEquals(true , items.contains(a3));
        System.assertEquals(true , items.contains(b3));
        System.assertEquals(false, items.contains(c3));
    }
}
5
  • Unless I'm missing something, doesn't this generate the hashcode based on the standard SObject hashcode? Meaning that your equals and hashcode methods will not agree as to the equality of the object. I'll test this when I can and see if it helps my performance issue. I'm already using the exact equality operator for equals (mentioned in my question), which works on its own but does not scale with set/map size. Aug 8, 2014 at 17:38
  • @AlexTennant "(1) If the hashCode method is invoked on the same object more than once during execution of an Apex request, it must return the same value. (2) If two objects are equal, based on the equals method, hashCode must return the same value. (3) If two objects are unequal, based on the result of the equals method, it is not required that hashCode return distinct values." This is directly from the docs, #3 applies here.
    – sfdcfox
    Aug 8, 2014 at 17:43
  • I'll give this a go, the pureist in me doesn't like the fact that equals and hashcode disagree, but if it's to spec I'll accept it begrudgingly. My only concern is that this won't speed up large numbers of put calls where I am adding values that look the same according to their hashcode, which is why I wanted to generate unique ones. In fact, this is essentially my original solution except you've implemented hashCode. Aug 8, 2014 at 17:48
  • @AlexTennant True enough, but that's kind of the point-- you have to implement both hashCode and equals for maps and sets to properly accept your logic as gospel.
    – sfdcfox
    Aug 8, 2014 at 17:52
  • Ok, so adding the hashCode implementation doesn't speed anything up. I did a bit more profiling and testing and it looks like the use of the exact equality operator is the issue, it is sloooooow, which is odd considering it should just be an integer compare behind the scenes. Unfortunately, this means I'm still not any closer and still need to figure out a way of generating a unique hash so I don't need to rely on ===. Aug 8, 2014 at 19:44
7

If I understand correctly...

You can set a static variable at 0 somewhere.

public class MyHashes
{
  private static Integer myHash = 0;

  //This lets nowhere outside control it and so you will get a new one each time
  public static Integer getNextHash()
  {
    ++myHash;
    return myHash;
  }
}

Modify your wrapper class.

private class SObjectReferenceWrapper
{
  public SObject Record;
  public Integer hash;

  public SObjectReferenceWrapper(SObject record)
  {
    this.Record = record;
    hash = MyHashes.getNextHash();
  }

  public Boolean equals(Object obj) 
  {
    if (obj instanceof SObjectReferenceWrapper) 
    {
        SObjectReferenceWrapper o = (SObjectReferenceWrapper)obj;
        return (hash == o.hash);
    }

    return false;
  }
}
4
  • 1
    This is beautiful.
    – sfdcfox
    Aug 8, 2014 at 17:16
  • This gives each SObjectReferenceWrapper instance a unique hashcode. It doesn't give each SObjectReferenceWrapper object that refers to the same SObject in memory the same hashcode, which is what I need. Aug 8, 2014 at 17:30
  • @AlexTennant This comment is exactly what I needed.
    – sfdcfox
    Aug 8, 2014 at 17:33
  • @AlexTennant If this is true, then his answer is more likely closer to what you need than mine.
    – dphil
    Aug 8, 2014 at 18:01
4

Summer 14 introduced a new System.hashCode(Object) static method that may do what you're looking for. I believe if your object implements hashCode() it invokes and returns the value provided, but if not it uses some internal definition that appears to be very quick.

5
  • No, this won't work. Note in the example where they do: System.assertEquals(-1394890885, System.hashCode(new Contact(LastName = 'Doe'))); twice to demonstrate that two different memory locations emit the same hash code. That's the exact opposite of what the OP is trying to achieve. They need stable, unique keys that will clearly identify a record uniquely, even if all fields match.
    – sfdcfox
    Aug 8, 2014 at 17:18
  • Brushing off some old memory of how this works in java and assuming apex is the same, don't maps check for hash code matches then check equals for matching hash codes? If I'm right that would give him the speed increase he needs, falling back to equals internally for anything critical. Aug 8, 2014 at 17:43
  • Native SObjects are "stupid", in that hashCode() and equals() will return true even if they are not the exact same record. For example new Contact().equals(new Contact()) returns true, but OP needs it to return false. Therefore, you must implement both hashCode and equals, and you need a wrapper class, just to make this work. Have some fun with this, too: map<contact, integer> values = new map<contact, integer>(); values.put(new contact(), 5); values.put(new contact(), 10); System.assertEquals(2, values.size()); // assert fails!
    – sfdcfox
    Aug 8, 2014 at 17:48
  • Very true, but I saw he's using a wrapper class where he can override them. Your example seems to cover this key bit better than mine does though. Aug 8, 2014 at 17:59
  • The devil is in the details. You need to return the wrapped object's hash code in order to have equals called, and then you need to return the memory-compare result from === in order to match by record. Only when both elements are present does it work like expected.
    – sfdcfox
    Aug 8, 2014 at 18:16
-1

I think the problem that you are having is that the ID is uninitialized (could be wrong, maybe its 0?) and Map uses the SObject's ID as the key.

It is not until they are inserted into the database that they are assigned UNIQUE Id's, even if they have the same information.

I think you need to insert your new accounts (or records in general) into the database with insert <record var>, the SObject reference will update automatically with the assigned unique ID, and map with thus be able to distinguish them.

2
  • Salesforce uses a combination of equals() and hashCode() to determine equality of map keys, not the Id. The reason it works when you have the Id is that because they are unique it causes a unique hashcode to be generated. In this case I specifically need to determine equality BEFORE I create records in the database, so inserting is not an option. Aug 8, 2014 at 17:17
  • You said you needed two SObjects in memory to have unique map keys. My interpretation was that 'in memory' meant the database.
    – JSF
    Aug 9, 2014 at 16:27

You must log in to answer this question.

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