13

I recently tripped across the need to take a list of objects (instances of a class--not sobjects) and split them into smaller lists of 200 members each.

The unit test below shows the code inside a unit test. It passes, but it's not really what I want.

I would like the signature for the method to be:

list<list<object>> listSplit(list<object> aList, integer size)

but the problem with that is a runtime conversion error:

System.TypeException: Invalid conversion from runtime type List<List<ANY>> to List<List<String>>

@isTest
public class TomTest
{
    // this unit test creates a list of 26 members
    // then calls listSplit to create chunks of 
    // 7 members each, except for the last which
    // has the left-overs.

    static testMethod void splitTest() {
        list<string> alphabet = new list<string> {
            'a', 'b', 'c', 'd', 'e', 'f', 'g',
            'h', 'i', 'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's', 't', 'u', 
            'v', 'w', 'x', 'y', 'z'
        };

        list<list<string>> octaves = listSplit(alphabet, 7);

        // I should have my list of lists and the
        // first three should have 7 letters each, 
        // and the last one only 5.
        system.assertEquals(4, octaves.size());
        system.assertEquals(7, octaves[0].size());
        system.assertEquals(7, octaves[1].size());
        system.assertEquals(7, octaves[2].size());
        system.assertEquals(5, octaves[3].size());
    }

    // here's the utility for splitting a list into
    // smaller parts, but rather than hard-coding the list type,
    // I would prefer something generic like list<list<object>>, 
    // and though the method works fine that way, I can't
    // cast the result back to list<list<string>> without throwing
    // a runtime conversion exception.
    static list<list<string>> listSplit(list<string> aList, integer size)
    {
        list<list<string>> collector = new list<list<string>>();

        integer i = 1;
        list<string> petiteList = new list<string>();

        for (string each : aList) {
            petiteList.add(each);

            if (Math.Mod(i, size) == 0) {
                collector.add(petiteList);
                petiteList = new list<string>();
            }
            i++;
        }

        if (petiteList.isEmpty() == false)
            collector.add(petiteList);

        return collector;
    }
}

2 Answers 2

14

You can't cast ANY to a more specific type, so you have to actually obtain the type that you want to return. Without proper reflection, you have to tell the function what type you'd like to return. There's several ways you can do that, but here's one way:

public class Parser {
    public static List<List<Object>> splitList(List<Object> items, Integer splitSize, Type destType) {
        // Take our destination type and cast to generic
        List<List<Object>> result = (List<List<Object>>)destType.newInstance();
        // Get a copy of source list to obtain list type.
        List<Object> protoList = items.clone();
        protoList.clear();
        // This is the list that will actually be added to result
        List<Object> tempList = protoList.clone();
        // A for loop with two counters.
        Integer index = 0, count = 0, size = items.size();
        while(index < size) {
            tempList.add(items[index++]);
            count++;
            // Split size reached, add to result and make new list
            if(count == splitSize) {
                result.add(tempList);
                tempList = protoList.clone();
                count = 0;
            }
        }
        // Add left-over partial
        if(!tempList.isEmpty()) {
            result.add(protoList);
        }
        return result;
    }
}

You use the code like this:

List<List<String>> v = 
    (List<List<String>>)Parser.splitList(
        myStringList,
        5, List<List<String>>.class
    );

It's a little messy, as you have to give it a primitive data type and then cast it back, but it works.

As an alternative, you might try the "out-param" approach:

public class Parser {
    public static void splitList(List<Object> items, Integer splitSize, List<List<Object>> result) {
        List<Object> protoList = items.clone();
        protoList.clear();
        List<Object> tempList = protoList.clone();
        Integer index = 0, count = 0, size = items.size();
        while(index < size) {
            tempList.add(items[index++]);
            count++;
            if(count == splitSize) {
                result.add(tempList);
                tempList = protoList.clone();
                count = 0;
            }
        }
        if(!tempList.isEmpty()) {
            result.add(tempList);
        }
    }
}

This one can't incur the run-time exception if destType isn't compatible with List<List<Object>> and saves you a bit of casting. You can use it like this:

List<List<String>> v = new List<List<String>>();
Parser.splitList(myStringList, 5, v);

The callee now becomes responsible for initializing the correct object type, but avoids the extra casting.

7
  • Ah, using clone. You could simplify the signature so you can pass in String.class and build up the composite Type from there.
    – Adrian Larson
    Commented May 18, 2016 at 4:39
  • 1
    @AdrianLarson I don't trust Type.forName to build composites for me. Sometimes they work, sometimes the compiler gets confused and finds the wrong class/data type (e.g. what happens when someone decides to name their class String?). By requiring someone to bind to a specific type, you take some of the guesswork out and things tend to go smoother.
    – sfdcfox
    Commented May 18, 2016 at 4:42
  • Well...String is definitely a reserved identifier. But I can't say I blame you, either.
    – Adrian Larson
    Commented May 18, 2016 at 4:44
  • @AdrianLarson True, but there's too many "classes" that don't get that protection. I remember once I was writing code and I decided to name a top-level class Schema...
    – sfdcfox
    Commented May 18, 2016 at 4:51
  • Thank you, @sfdcfox. The thing I was missing (in another version of my method), was using clone() to create another instance if the inner list with the exact type as the list passed in.
    – tggagne
    Commented May 26, 2016 at 1:50
1

Apex just doesn't have very robust support for generics. The best you can do in this particular instance is write a signature for each primitive (though Boolean and Blob are of dubious value). You can certainly extend it to include other types, which would be especially useful for interfaces, since they are more widely applicable.

It sucks, but if you find yourself performing this functionality often, it can be worth the effort to write this sort of utility. considering the

public class ListSplitter
{
    public static List<List<String>> split(List<String> values, Integer size)
    {
        // implementation
    }
    public static List<List<Id>> split(List<Id> values, Integer size)
    {
        // implementation
    }
    public static List<List<Integer>> split(List<Integer> values, Integer size)
    {
        // implementation
    }
    public static List<List<Decimal>> split(List<Decimal> values, Integer size)
    {
        // implementation
    }
    public static List<List<Double>> split(List<Double> values, Integer size)
    {
        // implementation
    }
    public static List<List<Long>> split(List<Long> values, Integer size)
    {
        // implementation
    }
    public static List<List<Date>> split(List<Date> values, Integer size)
    {
        // implementation
    }
    public static List<List<Datetime>> split(List<Datetime> values, Integer size)
    {
        // implementation
    }
}

It might be more extensible to do something like:

public class ListSplitter
{
    final List<List<Object>> results;
    ListSplitter(List<Object> input, Integer size)
    {
        this.results = new List<List<Object>>();
        List<Object> chunk = new List<Object>();
        for (Object instance : input)
        {
            chunk.add(instance);
            if (chunk.size() == size)
            {
                results.add(chunk);
                chunk = new List<Object>();
            }
        }
    }
    List<List<String>> getStrings()
    {
        List<List<String>> typedResults = new List<List<String>>();
        for (List<Object> chunk : results)
        {
            List<String> typedChunk = new List<String>();
            for (Object instance : chunk) typedChunk.add((String)instance);
            typedResults.add(typedChunk);
        }
        return typedResults;
    }
}

I played around with Type.forName and Type.newInstance, but couldn't make it more generic than that.

3
  • I have a working example, actually...
    – sfdcfox
    Commented May 18, 2016 at 4:23
  • 1
    @sfdcfox you would haha.
    – Adrian Larson
    Commented May 18, 2016 at 4:24
  • The part you missed was simply that you needed to construct the base types directly. I wouldn't trust Type.forName myself, but you can use newInstance to some effect. I used to spend way too much time messing with generics over the years.
    – sfdcfox
    Commented May 18, 2016 at 4:39

You must log in to answer this question.

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