15

I have a list of values, some of which could be lists/collections or single values. In JavaScript notation it might look like:

const input = [1,2,[3,4], [5,6], 7];

and I want to get:

const concatenated = [1,2,3,4,5,6,7];

So I have this Java code:

      ArrayList<T> concatenated = new ArrayList<>();

      for (T v : input) {
        try{
          concatenated.addAll((Collection) v);
        }
        catch (Exception e1){
          try{
            concatenated.addAll((List) v);
          }
          catch (Exception e2){
            concatenated.add(v);
          }
        }

     }

but that code seems pretty terrible to me. First I don't know if attempting to cast to List or Collection is sufficient - are there are other types I should attempt to cast to? Are there any errors I shouldn't ignore?

How to do this right?

  • 1
    fyi Java does not support mixed match type in list as you have in `input`, it can be like `input = [[1],[2],[3,4], [5,6], [7]];` – dkb Feb 11 '19 at 05:11
  • and there could be `null` values there (exception handling is for what otherwise)? – Naman Feb 11 '19 at 05:13
  • I think you can do this `concatenated.addAll((Iterable) v);` instead of try to cast to both `Collection` and `List`. –  Feb 11 '19 at 05:23
  • 2
    @MrCholo An array list does not have an overload for `addAll` with `Iterable`. The simplest interface you can use is `Collection` [See here](https://docs.oracle.com/javase/1.5.0/docs/api/java/util/ArrayList.html#addAll(java.util.Collection)). The reason being that `Iterable` doesn't have to persist the data. It can be a read once, or even random in what data is returned. All it specifies is that you can iterate on it. – flakes Feb 11 '19 at 05:42
  • This type of list operation is often called "flatten". – Fax Feb 11 '19 at 11:52
  • @Fax yes you're right, flatten or flatMap if you map –  Mar 05 '19 at 02:00

3 Answers3

13

The code doesn't need Exception handling as such unless there are null values in the lists. It should be sufficient though in your case to just cast basis of instanceOf as:

// Edit: Since the type of the input `Collection` is not bound strictly
List<Object> flatten(Collection<?> input) {
    List<Object> concatenated = new ArrayList<>();
    for (Object v : input) {
        if (v instanceof Collection) {
            concatenated.addAll(flatten((Collection<?>) v));
        } else {
            concatenated.add(v);
        }
    }
    return concatenated;
} 

using it further on jshell gives me this output:

jshell> List<Object> list = List.of(1,2,List.of(3,4),List.of(5,6),7) 
list ==> [1, 2, [3, 4], [5, 6], 7]

jshell> flatten(list)
$3 ==> [1, 2, 3, 4, 5, 6, 7]

:

svenmeier
  • 5,445
  • 15
  • 22
Naman
  • 23,555
  • 22
  • 173
  • 290
  • yeah but what about Collection? Can you use instanceof on Iterable? Collection and List both implement Iterable, so that might work –  Feb 11 '19 at 05:25
  • There might be nulls in the lists, how do I handle that? –  Feb 11 '19 at 05:25
  • @MrCholo that depends on the list you're adding to, with an `ArrayList` as in the current solution, `null`s would be accepted as well. – Naman Feb 11 '19 at 05:31
  • 1
    @MrCholo `null` is a valid argument to `instanceof`. it will always return false. And an `ArrayList` can accept `null` as an input to `add`. – flakes Feb 11 '19 at 05:33
  • 2
    I think a nice addition to this solution would be to make it recursive. ie `concatenated.addAll(concatenateList((List) v))` This way you can handle multiple levels of nesting. A better name on this method might be `flatten` – flakes Feb 11 '19 at 05:37
  • any reason why you are checking `v instanceof List` instead of `v instanceof Collection`? – Kartik Feb 11 '19 at 05:37
  • @Kartik none, started off from a specific use-case. – Naman Feb 11 '19 at 05:44
  • @flakes yes flattening is good addition, but would overcomplicate the question which doesn't really involve that part –  Feb 11 '19 at 05:45
  • if `v` is a List instance, will it also be a Collection instance? I didn't expect that –  Feb 11 '19 at 05:46
  • @nullpointer I think assuming that the array is `Collection input` is incorrect. OP is not using the collection as intended. Right now its a mixed set of classes. There are `T` elements and `Collection` elements. Because of this misuse of the type system, I think you should just strip this to the non specialized format `Collection input` – flakes Feb 11 '19 at 05:47
  • 1
    @MrCholo `List` extends `Collection` – Naman Feb 11 '19 at 05:47
  • @flakes In my understanding keeping the type bound should avoid exception handling for a class cast at least. Hence would prefer to keep it instead of all raw types other wise. – Naman Feb 11 '19 at 05:50
  • @nullpointer even in the example you provided, you use `List` as the collection. `Collection` doesn't add any value because `T` always has to be `Object` for any practical use of the method. If the input `T` was not `Object`, then the contract of the list has already been broken – flakes Feb 11 '19 at 05:58
  • At the very least, use `Collection>` as the input and `List` as the result. – flakes Feb 11 '19 at 06:02
  • @flakes the example provided could be using specific type as well `List list = List.of(1,2,9,10,7)` or even `List> list = List.of(List.of(1,2), List.of(3,4))` and it would still work. Why should it be required to specify an `Object` there? Also, I really don't see an output of `List` of `String` and `Integer` in the question as well. – Naman Feb 11 '19 at 06:11
  • Because the expected input to this method, say `List.of(1, List.of(2,3))`, is neither a `List` or a `List>` – flakes Feb 11 '19 at 06:12
  • Further, if the input was `List>` then the output expected would be `List>`, which is incorrect because the transformation changes it to `List`. And if the input was already a flat `List` there would be no point in calling the method at all – flakes Feb 11 '19 at 06:18
  • @flakes hmm, well I get the point of `List>` input. Thanks. Just that now I might just try and find a better generic solution maybe. – Naman Feb 11 '19 at 06:22
  • 1
    @nullpointer It's an interesting problem! I have tried to solve this problem before (when dealing with examining JSON data without having a schema ahead of time) and I don't think Java has a good model for dealing with this. The root problem is that a basic `Collection` or `List` or `Map` interface does not account for nested hierarchies of data when specializing the container. Other languages combat this problem differently, say through the use of pattern matching rather than strict class matching. A good example of tackling this is in Python's new type hints system. – flakes Feb 11 '19 at 06:32
5

As others have mentioned, using exceptions for control flow is not ideal. You can instead use the instanceof operator to test if an element is a Collection. The answer by nullpointer shows a good example of this. If you want an more generic option you could also do something like:

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

public static <E> List<E> deepFlatten(final Iterable<?> iterable, final Class<E> type) {
    if (type.isPrimitive() || type.isArray() || Iterable.class.isAssignableFrom(type)) {
        throw new IllegalArgumentException(
                "type must not denote a primitive, array, or java.lang.Iterable type: " + type);
    }
    final List<E> result = new ArrayList<>();
    for (final Object element : iterable) {

        if (element instanceof Iterable<?>) {
            result.addAll(deepFlatten((Iterable<?>) element, type)); // recursion

        } else if (element != null && element.getClass().isArray()) {

            if (element instanceof Object[]) {
                result.addAll(deepFlatten(Arrays.asList((Object[]) element), type)); // recursion
            } else { // primitive array
                final Iterable<?> itrArray = IntStream.range(0, Array.getLength(element))
                        .mapToObj(index -> Array.get(element, index))::iterator; // method reference
                result.addAll(deepFlatten(itrArray, type)); // recursion
            }

        } else {
            /*
             * Will throw ClassCastException if any element is not an instance
             * of "type". You could also throw a NullPointerException here if
             * you don't want to allow null elements.
             */
            result.add(type.cast(element));
        }

    }
    return result;
}

This also handles "embedded" arrays, as well as Iterables, through recursion. Note it doesn't handle Maps because of the ambiguity; should we flatten the keys or the values—or both?

Calling the above with:

Iterable<?> iterable = List.of(
        "A", "B", "C", "D",
        List.of("E", "F", List.of("G", "H"), "I", "J"),
        "K",
        new String[]{"L", "M", "N", "O", "P"},
        new String[][]{{"Q", "R"}, {"S", "T"}, {"U"}, {"V"}},
        new Object[]{"W", "X"},
        "Y", "Z"
);
List<String> flattened = deepFlatten(iterable, String.class);
System.out.println(flattened);

Gave me:

[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]

Note that the letters are in order because Lists and arrays have guaranteed iteration orders. If your Iterable contained a Set the result of the deepFlatten may not be in the same order each time.

Slaw
  • 25,955
  • 5
  • 33
  • 58
2

Use of Exceptions to control application flow/business logic is an anti-pattern. You can read more about it here, here and here.

Regarding storing different types of elements in Collections could be difficult to debug and maintain. You can write your own wrapper and encapsulate the handling of it from usage. You can refer this for an inspiration.