Method to flatten generic array

Viewed 230

I've been trying to write a method that will flatten a generic array if it is nested.

    private static <T> List<T> flatten(T[] in) {
        List<T> result = new ArrayList<>();
        for (T e : in) {
            if (e.getClass().isArray()) {
                result.addAll(Arrays.asList(e)); ## Issue is here. 
            } else {
                result.add(e);
            }
        }
        return result;
    }

This code does not cause any errors but also does not work. When e is not an array, things work as expected... a list is populated with the elements of in and returned.

However when e.getClass().isArray() == true, the elements of e are not added. Rather the original array is added so I end up with a list of arrays.

My use case here is that I have a method that is being passed generics T[] someArray

public <T> void doSomeStuff(T[] someArray) {
   Set<T> unique = Sets.newHashSet(someArray)
   ... do some stuff with the unique values ... 
}

The input someArray may either be nested or not (i.e. T itself may be an array, resulting in T[][]). I want to determine the unique elements contained in the input, whether or not it is nested. Passing the input someArray to a set only works if it's not nested, hence I'm trying to flatten.

So my question is, how can I do this and why is my method above not working? Thanks in advance for the edcuation.

1 Answers

Your code can't work. The generics just don't line up.

Let's say you have an array that is a combination of strings and arrays of strings. That cannot possibly be a T[] unless T is object, which isn't what you want (as that would mean you get a List<Object>. After all, If T is String, then your input array, which is defined as T[] in, is a String[] in, which cannot contain string arrays. After all, a String[] is not a subtype of String, for obvious reasons.

It is impossible to describe in terms of generics the concept of 'an array of Strings, or an array of arrays of Strings, or an array of arrays of arrays of Strings, and so forth'. So, generics have no place here. If you want that, all you can 'type' is 'an array whose component type is unknown and hybrid anyway', which is Object[] in java (this is co/contra-variance wise broken, but this is just part of the java spec: Variance on arrays is incorrect, known problem and not fixable).

This gets you a secondary issue: Generics are erased, and in that model you don't have an actual type to work with. In fact, because it is impossible to use generics to tell the compiler to do some type checking on the input array, there is nothing the compiler can do for you, so any type checking you want (and you clearly want that, you don't want to return a List of who knows what this is), will have to be done at runtime.

Unfortunately, it is impossible to do that, too - you can't check if at runtime if some object is, say, a Map<String, Integer>.

So, what you want is impossible.

It becomes possible if you're okay with this method being only able to do the job for reified types. That is, types that don't contain any <> themselves. So, if you want to take 'an array that contains a combination of "Map of string to integer" and "arrays of Maps of string to integer"', this method will not be able to do that and it is in fact completely impossible to do such a thing in java. But if you're okay with, say, "An array containing a combination of strings and arrays of strings" and want to turn that into a flattened-out list of strings, okay, that's possible.

It's complicated, though:

public <T> List<T> flattenArray(Class<T> type, Object[] in) {
    if (type.isArray()) throw new IllegalArgumentException();
    var out = new ArrayList<T>();
    flattenArray0(type, in, out);
    return out;
}

private <T> void flattenArray0(Class<T> type, Object[] in, List<T> out) {
    for (Object a : in) {
        if (a == null) {
            out.add(null);
        } else if (a.getClass().isArray()) {
            flattenArray0(type, (Object[]) a, out);
        } else {
            out.add(type.cast(a));
        }
    }
}

In action:

Object[] test = new Object[3];
test[0] = "Hello";
test[1] = new String[] {"Foo", "Bar"};
Object[] threeDeep = new Object[2];
test[2] = threeDeep;
threeDeep[0] = "Goodbye";
threeDeep[1] = new String[] {"Baz"};

List<String> result = flattenArray(String.class, test);
System.out.println(result);

should print: ["Hello", "Foo", "Bar", "Goodbye", "Baz"].

Related