5

I'm trying to cast a Map<Object, List<SObject>> to Map<String, List<Account>>, which fails with the error

Incompatible types since an instance of Map<Object,SObject> is never an instance of Map<String,Account>

That's because Apex likely gets transpiled to Java where Map is a compile-time abstraction. Unlike Java, we can't force the issue by writing something like

Map<Object, List<SObject>> foo = ... // some initialization
Map<String, List<Account>> bar = (Map) foo;

because raw Map doesn't exist in Apex. Can this be worked around somehow? The end goal is to have a Map<Object, List<SObject>> return type which would allow a method to return maps of various types of SObjects.

  • 1
    The Type system is fundamentally broken. See this question where I delve further into the limitations of the current Type system. – sfdcfox Jan 12 '17 at 22:08
  • The only way is avoid this design. This is broken in Apex. You can only control this during iteration (for-loop). – Ashwani Jan 12 '17 at 22:17
  • It's very different when your key type is SObject vs List<SObject>. Where are you getting the structure from? Are you trying to write a mapping/grouping utility? – Adrian Larson Jan 12 '17 at 22:24
  • @AdrianLarson I am, yes. I'd like to group sObjects by some field other than Id. – ipavlic Jan 12 '17 at 22:32
  • 1
    Yes, I see now, because you can implicitly cast List<sObject> to List<Account> (!) but not sObject to Account. I'll edit the question again to match the answers. – ipavlic Jan 13 '17 at 18:12
5

The suggestion is to not use this type of polymorphism. You can't store specific collections in generic collections, but you can store generic collections in specific collections, which risks the odds of Bad Things™ happening. However, if you're willing to take the possibility of logic errors, you can always use the maps by only using generic collections.

Map<Object, SObject> keyValueMap = new Map<Object, SObject>();
keyValueMap.put("Hello", new Account());
keyValueMap.put("World", new Contact());
keyValueMap.put(42, new Opportunity());

You can still retrieve and set keys in various ways, but this means you'll frequently to either cast or use instanceOf on individual keys or values, which can make code more challenging to read. Also, if you decide to use List<SObject>, you lose the ability to upsert as well.


You can workaround that by using some clever tricks, though:

public static Map<Object, List<SObject>> populateKeyValueMap(SObject[] records, Processor process, Type sobjectListType) {
    Map<Object, List<SObject>> results = new Map<Object, List<SObject>>();
    for(SObject record: records) {
        Object key = process.findKey(record);
        if(!results.containsKey(key)) {
            results.put(key, sobjectListType.newInstance());
        }
        results.get(key).add(record);
    }
    return results;
}
...
Map<Object, List<SObject>> results = populateKeyValueMap(accountList, new MapByNameProcessor(), List<Account>.class);
for(Object key: results.keySet()) {
    // Funny bug: no cast required here
    Account[] accountNameList = results.get(key);
    // more processing here
}

By using the dynamic System.Type, you can still have concrete lists, which lets you use upsert normally if you want to, as well as using static field references, etc. But, like I said, it would be easy to screw up, so if you plan on abusing the type system, make sure you unit test your code to death to avoid pesky runtime behavior.


Edit: Also, saw your comment; you could define the method as follows:

public static Map<Object, List<SObject>> groupByField(SObject[] records, SObjectField field, Type concreteListType) {
    Map<Object, List<SObject>> results = new Map<Object, List<SObject>>();
    for(SObject record: records) {
        if(!results.containsKey(record.get(field))) {
            results.put(record.get(field), concreteListType.newInstance());
        }
        results.get(record.get(field)).add(record);
    }
    return results;
}

This is actually a method I do use, but you need to actually caution future developers about the dangers of the methods with comments.

  • That Account[] accountNameList = results.get(key) works is pure comedy gold! I don't have a problem with populating the map, I would just like to be able to cast it to appropriate type so that I can inspect it later. Because of the // Funny bug, it actually works without a cast. So a bug is shadowed by a bug. Amazing, the work they've done with the type system. – ipavlic Jan 12 '17 at 22:37
  • @ipavlic I added an example method that demonstrates how the utility method might look, too. Feel free to use it if you want to, just make sure you unit test your code. – sfdcfox Jan 12 '17 at 22:40
  • @ipavlic Unfortunately, you can never directly cast the entire collection. You could round-trip through JSON serialize/deserialize, but it'd be incredibly expensive CPU-wise. – sfdcfox Jan 12 '17 at 22:41
  • The utility in question is meant for github.com/ipavlic/salesforce-lambda. Interestingly, the bug you've described actually makes it possible to use a generic filter without casting! So we're actually allowed to say something like List<Account> lowRevenue = Filter.field(Account.AnnualRevenue).lessThanOrEquals(cutoff).apply(accounts); because apply returns an SObject list. – ipavlic Jan 12 '17 at 22:42
  • 1
    @ipavlic You can, but then you lose the ability to upsert without copying all the records to a new concrete list. – sfdcfox Jan 12 '17 at 22:52
2

I usually work around it by using concrete key types. It's not a ton of extra code to support primitives.The grouping pattern is really simple to lay out in a way that will work for any field. A basic utility can still be achieved in relatively few lines using the following pattern:

public class GroupBy
{
    public Map<String, List<SObject>> strings(String field, List<SObject> records)
    {
        Map<String, List<SObject>> grouped = new Map<String, List<SObject>>();
        for (SObject record : records)
        {
            String key = (String)record.get(field);
            if (!grouped.containsKey(key))
                grouped.put(key, new List<SObject>());
            grouped.get(key).add(record);
        }
        return grouped;
    }
}

This pattern is really simple to extend and use. I think you could figure out how to implement decimals, ids, etc. It also has the added benefit of working well with filters:

Map<Id, List<MyObject__c>> groupedRecords = new Map<Id, List<MyObject__c>>();
List<Parent__c> parents = [SELECT Id FROM Parent__c WHERE Id IN :groupedRecords.keySet()];

I find this use case alone a compelling argument for concrete key types.

By the way, casting is permissive with List<SObject> types, so you can do:

Map<Id, List<UserRole>> roleHierarchy = GroupBy.ids('ParentId', [
    SELECT ParentId FROM UserRole
]);

You can also add bells and whistles like concrete type instantiation, SObjectField and cross-object support, etc.

  • I like your naming convention a lot. Is it okay if I reuse it in the actual utility I'm working on (github.com/ipavlic/salesforce-lambda)? – ipavlic Jan 12 '17 at 23:11
  • I didn't come up with it by any means. IANAL but the concept is so simple I can't imagine it would be an issue under MIT license or some such. It was kind of an organic extension of our Pluck utility, which I see you already have. – Adrian Larson Jan 12 '17 at 23:17

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.