10

To reduce code duplication, at the risk of type safety, I have some method:

public Map<Object, Object> doSomething(Map<Object, Object> inputMap) {
    Map<Object, Object> outputMap;
    // Does something.
    return outputMap;
}

The method does what I expect it to do.

The method which calls this method needs to convert an instance of Map<String, SObject> to doSomething() and, if/as/when doSomething() works as expected, we can expect all the keys will be String and all the values will be SObjects.

The problem is, even though this is an entirely realistic scenario, it isn't one recognized by the Apex compiler, which will reject:

Map<String, SObject> map1 = new Map<String, SObject>();
Map<Object, Object> map2 = (Map<Object, Object>) map1;

With

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

This really doesn't make much sense since I would expect every String to be an Object and every SObject to be an Object, and therefore I would expect every Map to be an instance of Map<Object, Object>.

Less puzzling, though no less false for the absolutism, and equally problematic,

Map<Object, Object> map1 = new Map<Object, Object>();
Map<String, SObject> map2 = (Map<String, SObject>) map1;

will result in

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

Of course, I can work-around these errors by looping though each map, but that's ugly, inefficient, and I'd need to create a pair of such loops for every consumer of my method.

Is there a good/better way to do this?

  • 3
    As a side note, if you're wondering why you're getting these errors, I asked (and answered) a question related to this about a year and a half ago: salesforce.stackexchange.com/questions/145428/… – Derek F May 9 '18 at 13:10
  • @DerekF, Cheers for the response. I remember your sharing this in response to another question. :-) Unfortunately, in this instance, that article is less helpful. – Brian Kessler May 9 '18 at 13:20
8

Looking in to this question, I realized that the type system is inherently broken. They have "fixed" Maps, it seems, but in a way that no longer lets you mix and match some types, especially Object.

The list class is still broken, however:

class c1 {

}
class c2 extends c1 {

}
c1[] s = new c2[0];

Generally speaking, unless/until they fix the problem, you won't be able to be polymorphic this way; your classes that intend to call this class would also need to use Map<Object, Object>, which will involve some casting.

One thought I had would be to create an interface like this:

public interface keyValueMap {
    Set<Object> getKeys();
    Object[] getValues();
    Object getKey(Object value);
    Object setKey(Object key, Object value);
}

And then create specific wrappers:

public class StringSObjectMap implements KeyValueMap {
    Map<String, SObject> values;
    public StringSObjectMap(Map<String, SObject> values) {
        this.values = values;
    }
    public StringSObjectMap() {
        values = new Map<String, SObject>();
    }
    public Set<Object> getKeys() {
        Set<Object> results = new Set<Object>();
        for(String value: values.keySet()) {
            results.add(value);
        }
        return results;
    }
    public Object[] getValues() {
        return (Object[])values.values();
    }
    public Object getKey(Object value) {
        return (Object)values.get((String)value);
    }
    public Object setKey(Object key, Object value) {
        return values.put((String)key, (SObject)value);
    }
}

Note that since we can't cast Set<SObject> to Set<Object>, we have to build a duplicate key set. Make sure you cache the results for performance reasons.

Finally, you can write your method to use the new interface:

public keyValueMap doSomething(keyValueMap inputMap) {

And to call it, you'd pass in the appropriate sub-wrapper:

keyValueMap results = doSomething(new StringSObjectMap(recordMap));

Depending on what you're doing, this may require additional post-processing to get the values back out.

You might also just leave the return type as Map<Object, Object> but you'll end up having to process the keys again, as demonstrated in the wrapper above. One additional step might help you here; you can pass in the type of return value you want:

public keyValueMap doSomething(keyValueMap inputMap, Type mapType) {
  keyValueMap results = (keyValueMap)mapType.newInstance();

Called as:

keyValueMap results = doSomething(new StringSObjectMap(recordMap), StringSObjectMap.class);

You could also create a custom Iterator, but this doesn't work in for-each loops (but you can use iter.next/iter.hasnext):

public class KeyValue {
  public Object key;
  public Object value;
  public KeyValue(Object k, Object v) {
    key = k;
    value = v;
  }
}

public class StringSObjectIter implements Iterable<KeyValue>, Iterator<KeyValue> {
    Map<String, SObject> values;
    String[] keys;
    public StringSObjectIter(Map<String, SObject> values) {
        this.values = values;
    }
    StringSObjectIter(StringSObjectIter copy) {
        values = copy.values;
        keys = new List<String>(values.keySet());
    }
    public Boolean hasNext() {
        return !keys.isEmpty();
    }
    public KeyValue next() {
        String k = keys.remove(0);
        return new KeyValue(k, values.get(k));
    }
    public Iterator<KeyValue> iterator() {
        return new StringSObjectIter(this);
    }
}

Which you can use in your method as:

public keyValueMap doSomething(Iterable<KeyValue> inputs) {
  Iterator<KeyValue> iter = inputs.iterator();
  while(iter.hasNext()) {
    KeyValue value = iter.next();
    ...

Calling method for this:

keyValueMap results = doSomething(new StringSObjectIter(recordMap));
  • Thanks for the very detailed answer, but I see a lot of overhead and ultimately I still see you are using loops (e.g. for getKeys()), so I can't help wondering if this doesn't make the cure worse than the disease? Would we at least expect better performance and/or memory usage from such a solution? Also, is there any information available regarding when we are/are-not allowed to caste maps from one type to another? (Also, does SFDC have a plan to fix the type system? Is there an idea we can vote for?) – Brian Kessler May 9 '18 at 19:54
  • 2
    @BrianKessler There's no plan to fix as of yet; I've talked to the PM and they're swamped with other features right now. WIthout casting, an Iterable/Iterator wrapper class should have close-to-optimal performance that's not much worse than iterating over the keyset in a for loop, and the KeyValueMap idea should have decent performance as well. Maybe I'll get a moment to do some performance testing. – sfdcfox May 9 '18 at 20:09
  • I'm just thinking about your proposed wrapper and have two further questions: 1. Would it be possible to get the Type from the instance of KeyValueMap which is passed as a parameter, if we know we want to return the same type as we passed in? 2. Also, if I want to have an abstract KeyValueMap which my concrete KeyValueMaps inherit from, would it be possible for iterator() to live on the abstraction? – Brian Kessler May 9 '18 at 21:34
  • @BrianKessler 1. You could pass in the type, or even pass in a blank output value doSomething(keyvaluemap input, keyvaluemap output) (this works because objects are passed by reference), or pass in the type, as in this answer. (2) An abstract keyvaluemap could indeed have an iterator method to consolidate the code a little bit. Honestly, if they'd just give us back parameterization, this could be so much easier. – sfdcfox May 9 '18 at 21:49
  • 2
    @BrianKessler Parameterized classes were removed in Winter '13 (outlined here). The reason why wasn't given, but apparently it was "extremely broken." I wish they'd fix types, and also fix parameterization. I can ask the PM about it, but we're not likely to see either any time soon. It's unfortunate that Apex Code is so much more primitive than Java itself, but it's what we have to work with. The interface idea isn't too bad; I added it to my library. – sfdcfox May 10 '18 at 5:10
3

No, there isn't a better way. With Set, you can hack around it to some extent. But with a Map, you care about the values, not just the keys, so you can't do it without looping.

Set<String> values = new Set<String>();
Set<Object> generic = new Set<Object>((List<Object>)new List<String>(values));

It's not immediately clear if this song and dance is even faster than looping anyway.

  • Hi. Thanks for the response. It's not immediately clear for me how the "song and dance" might provide an alternative for "casting" the map without looping, since as you point out, I do care about both the values and the keys. – Brian Kessler May 9 '18 at 13:19
  • 2
    That's what I'm saying. There is no alternative to looping for Map. – Adrian Larson May 9 '18 at 13:19
  • You might be right; you probably are. But just in case somebody knows something that both of us are missing, I'm going to leave this question without an accepted answer. – Brian Kessler May 9 '18 at 13:26

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.