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.

9
  • 1
    The Type system is fundamentally broken. See this question where I delve further into the limitations of the current Type system.
    – sfdcfox
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 at 22:24
  • @AdrianLarson I am, yes. I'd like to group sObjects by some field other than Id.
    – ipavlic
    Commented Jan 12, 2017 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
    Commented Jan 13, 2017 at 18:12

2 Answers 2

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.

7
  • 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
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 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.

2
  • 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
    Commented Jan 12, 2017 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
    Commented Jan 12, 2017 at 23:17

You must log in to answer this question.

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