18

As most stories here begin, I was writing some code for a project that I was working on, and stumbled across an oddity when writing some unit tests. I was testing a method that has a Set<sObject> parameter, and I was attempting to pass this method a Set<Account>.

As Account inherits from sObject, I thought this would work fine. However, when running my test, I got the error

Method does not exist or incorrect signature: [TestClass].someMethod(Set<Account>)

Yes, the method that I was testing expects an argument of Set<sObject>, but an Account can be up-cast to an sObject, and Salesforce handles that implicitly, right?

Since Salesforce was throwing a fit when I tried to rely on implicit casting, the next thing I tried was explicitly up-casting. When I tried to explicitly up-cast my set, I was greeted with a different error

Line: someNumber, Column: otherNumber Incompatible types since an instance of Set<Account> is never an instance of Set<SObject>

This can be reproduced with the following snippet

Set<sObject> testSet1;
Set<Account> testSet2 = new Set<Account>();

testSet1 = (Set<sObject>)testSet2;

I haven't found anything in the documentation to suggest that up-casts for sets aren't allowed. Given that we can up-cast Lists and Maps, I feel this is rather unexpected.

Furthermore, knowing that an Apex set is really a Java HashSet in the back-end (look at the bottom of this page of documentation), I tried to look for documentation to see if this is somehow dictated by Java, but I came up empty there as well.

I've been able to move on with my project, but this leaves me wondering...

Is there some documentation (Salesforce or Java) that explains this behavior? Failing that, can someone cobble together a feasible explanation for why Sets can't be up-cast?

  • Short answer: because they're broken. I don't think you'll find documentation on it. – sfdcfox Oct 21 '16 at 19:34
  • You shouldn't really use Set<SObject> in general. – Adrian Larson Oct 21 '16 at 19:35
  • @AdrianLarson Yep, I know. I was trying to provide a convenience method that would call the version of the same method that takes a List<sObject> – Derek F Oct 21 '16 at 19:43
  • Hence, perhaps, reason why Force.com Enterprise Architecture patterns always pass set<ID> around. :-) – cropredy Oct 21 '16 at 23:30
12

After doing a deeper dive with google, I think I have the answer.

It turns out that this is a behavior defined by Java to ensure type safety. In Java, a Set<String> is a 'Generic' collection. Generics in Java cannot be up-cast (generics are not covariant) because (in part) Java erases the type information (of generics) upon compilation to bytecode (known as Type Erasure).

There are several resources that I found that explain this:

Because a Set<String> in Apex is implemented as a Java Set<String> (using the HashSet data structure, though that bit isn't important), Apex is subject to this restriction of Java.

So why are Lists and Maps in Apex are just fine with up-casting?

E.g. The following will compile and then fail at runtime in Apex.

List<Sobject> sobjs = (List<Sobject>)(new List<Account>());
sobjs.add(new Account());
sobjs.add(new Contact());

System.TypeException: Collection store exception adding Contact to List

I think this speaks to how Salesforce implements these two collection types.

Salesforce doesn't directly mention how Lists and Maps are implemented in the documentation (unlike the documentation for Sets), but based on how we are able to declare Lists like this

String[] stringList = new List<String>();

And access list indices like this

String result = stringList[0];

I'd have to say that I think Lists in Apex are actually Java arrays (which are covariant, and therefore can be up-cast) with some extra bits to hide the fact that Java arrays have a static size.

Maps are...I have no idea.

The following snippet works

Map<Id, String> testMap1 = new Map<Id, String>();
Map<Id, Object> testMap2 = (Map<Id, Object>)testMap1;

While this snippet won't compile

Map<Id, String> testMap1 = new Map<Id, String>();
Map<Object, String> testMap2 = (Map<Object, String>)testMap1;

The methods exposed by the Java Map interface look very similar to the Map methods that Salesforce offers. Perhaps Apex Maps end up using Java type wldcards, or are a custom implementation that stores the keys in a set, and the values in an array (and has something to map keys to indices in the values array).

  • Nice read. Still trying to wrap my head around covariance. Hadn't read up on that one before. – Adrian Larson Oct 21 '16 at 21:31
  • Yeah, type systems/theory (of which covariance is a part) for me crosses the line between understandable computer science, and impermeable formal mathematics. – Derek F Oct 21 '16 at 21:54
  • With regards Lists and Arrays being interchangeable in Apex. I think Arrays are actually Lists with bounds checking. You can see it in the debug log on the VARIABLE_SCOPE_BEGIN event where a String[] variable actually appears as a List<String>. Also, if you out of bounds an Apex array you get a System.ListException – Daniel Ballinger Oct 26 '16 at 19:10

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.