3

I have a generic method that sets up some objects and returns a list of them. The type of the object is defined by an interface.

The interface:

public interface DynamicThing {
   DynamicThing configure( Map<String,Object> settings );
}

The method that builds the List:

public List<DynamicThing> buildList( String type, List<Map<String,Object>> settings ) {

    List<DynamicThing> things = new List<DynamicThing>();

    for ( Map<String,Object> thisSettings : settings ) {
        things.add(
            ((DynamicThing)System.Type.forName( type ).newInstance())
                .configure( thisSettings )
        );
    }

    return thisThing;
}

This all works fine, and I can use buildList to dynamically build a list that contains things that implement DynamicThing.

The problem is, when I get that list back, if I want to treat it as the concrete version, I first need to build a new list of that type and cast the bits inside.

That is if I have a class:

public class ConcreteThing implements DynamicThing {
    public DynamicThing configure( ... )
}

If I want to build a list dynamically and get the things back and treat them as a List<ConcreteThing>, I need to:

List<DynamicThing> rawList = buildList( 'ConcreteThing', thingSettings );
List<ConcreteThing> newList = new List<ConcreteThing>();
for ( DynamicThing thisThing : rawList ) {
    newList.add( (ConcreteThing)thisThing ) );
}

I'd like to just be able to cast the List. E.g.:

List<ConcreteThing> concreteThings = (List<ConcreteThing>)buildList( 'ConcreteThing', thingSettings );

In order to do that, I'd need to dynamically create the List with the right type.

How do I do that?

1 Answer 1

2

Even though it might not be immediately obvious, we can use System.Type.forName to dynamically build lists instances too.

For example, we can:

System.Type.forName( 'List<ConcreteThing>' ).newInstance()

In turn, this means we can build that List name up:

System.Type.forName( 'List<' + type + '>' ).newInstance()

That therefore allows us to build our list with the right type.

However, that might seem to only be half of the battle - we have the right type of list, but we have a generic variable (List<DynamicThing>) and we can't dynamically declare the type of variable, nor dynamically cast the individual instances that we're creating to the required concrete type.

It turns out, we don't have to, and the following works:

public List<DynamicThing> buildList( String type, List<Map<String,Object>> settings ) {

    List<DynamicThing> things = (List<DynamicThing>)System.Type.forName( 'List<' + type + '>' ).newInstance();

    for ( Map<String,Object> thisSettings : settings ) {
        things.add(
            ((DynamicThing)System.Type.forName( type ).newInstance())
                .configure( thisSettings )
        );
    }

    return things;
}

As long as the passed in 'type' is an implementation of DynamicThing, everything is OK...

When we build the list, we get a List<ConcreteThing>, which (according the the Apex type system) we can treat as a List<DynamicThing>.

As far as the Apex compiler is concerned, we are now allowed to put any implementation of DynamicThing into the List.

This strictly speaking isn't true, since we should only be allowed to put in ConcreteThing instances. However, the compiler is OK with this relaxing of the rules and we would get an exception at runtime if we ever tried to put in an instance that wasn't a ConcreteThing.

However, since we always build the instances in the same method, and do so with the same type variable is the input, we know we'll always put in objects of the right type.

So, we build our instances dynamically and state that they are implementations of DynamicThing. They are - ConcreteThing is an implementation of DynamicThing, so this is valid - and we put them into the list.

The result is:

  • We have built a List<ConcreteThing> and added instances of ConcreteThing into it.
  • The compiler thinks we have a List<DynamicThing> (not strictly speaking true) and added instances of DynamicThing into it (we have).

The result is that whilst the method states that it returns a List<DynamicThing>, it actually returns a List<ConcreteThing>, and so it can be cast with:

List<ConcreteThing> concreteThings = (List<ConcreteThing>)buildList( 'ConcreteThing', thingSettings );

Personally, I think this is an example of how the type system in Apex is broken: a List<ConcreteThing> is not a List<DynamicThing>, it's just that the things IN the list are DynamicThing implementations. But - it works, and it's useful in this scenario.

5
  • Since the calling program apparently is able to pass the string ConcreteThing, why can't you pass a Type rather than a String, e.g. List<ConcreteThing>.class ?
    – cropredy
    Commented Mar 9, 2023 at 17:01
  • I could, but then I'd need 2 types - the singular and the list, and they could potentially be out of step. As a string they're both represented by the same parameter. Commented Mar 9, 2023 at 21:31
  • Plus, in the real example, it IS a string, it's entirely dynamic based on data from the database. Commented Mar 9, 2023 at 21:32
  • 1
    works for me given your app requirements; I figured anyone who could compose Amoss would have realized that but sometimes stuff gets overlooked
    – cropredy
    Commented Mar 9, 2023 at 22:57
  • Thanks, but yeah, you'd be surprised! Commented Mar 10, 2023 at 7:44

You must log in to answer this question.

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