20

Is there a possibility to check if an object is either an array or a collection with one clause? What I am trying to achieve:

Assuming arrays implement Iterable, and assuming the Object foo could be either an array or a collection, I'd like to use a code snippet like this:

if (foo instanceof Iterable) {
  for (Object f : (Iterable) foo) {
    // do something with f
  }
}

Unfortunately, an array cannot be cast to Iterable. Nor does it implement Collection. Are there any other possibilities to handle both in one loop like the above? Instead of -- of course -- using an if-else if-clause and two loops (which wouldn't be nice).

Edit: In response to these answers. I am aware of the isArray() method but in this case the casting in

...
for (Object f : (Iterable) foo) {
...

will fail. That'd a pity and a code redundancy since I would have to use two loops although a foreach-loop works both with Collections and Arrays.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Pawel Os.
  • 611
  • 8
  • 26
  • The way I see it, `foo` must be declared as `Object`. So you have to cast anyway. Are you trying to cast just once? – ernest_k Apr 09 '19 at 08:35
  • The foreach-loop won't work if I cast it to Object -- you cannot iterate through objects. – Pawel Os. Apr 09 '19 at 08:39
  • 9
    It's a good idea to avoid a design that leads to this in the first place. Even if you had a common interface for this to work with just one for-each loop, you'd still have many type casts. Your code wouldn't be type-safe (`Object[]`? raw `Iterable`?) – ernest_k Apr 09 '19 at 08:52

6 Answers6

18

Regarding a condition to check if foo is either a collection or an array:

Class#isAssignableFrom may come in handy.

Class<?> fooClass = foo.getClass();
boolean isArrayOrCollection = Collection.class.isAssignableFrom(fooClass) ||
                              Object[].class.isAssignableFrom(fooClass);

I reasonably assume you won't test it on primitive arrays since you have collections which work only with the wrapper classes.

I guess you can safely replace Object[].class.isAssignableFrom(fooClass) with fooClass.isArray()

boolean isArrayOrCollection = Collection.class.isAssignableFrom(fooClass) ||
                              fooClass.isArray();

and it would also work for a primitive array class.


I've run a small "test"

class Test {
    public static void main(String[] args) {
        Predicate<Class<?>> p = c -> Collection.class.isAssignableFrom(c) || 
                                     c.isArray();

        System.out.println(p.test(new int[0].getClass()));
        System.out.println(p.test(new Integer[0].getClass()));
        System.out.println(p.test(Collections.emptyList().getClass()));
        System.out.println(p.test(Collections.emptySet().getClass()));

        System.out.println(p.test(Collections.emptyMap().getClass()));
    }
}

which results in

true
true
true
true
false

Regarding a generic loop that would run over both arrays and collections:

You simply can't write an accurate construction to handle this: Collection (or Iterable) and Object[] have little in common (Object as a common parent and its methods are not enough).

I think it's sensible to build own abstraction which would treat collections and arrays in the same manner. Having no particular context, I can come up with a simple idea of two subclasses, each of which defining how its source (either a collection or an array) should be iterated. Then, programming to an interface will help to manage them equally.

A very simplified example would be:

interface Abstraction<T> {
    void iterate(Consumer<? super T> action);

    static <T> Abstraction<T> of(Collection<T> collection) {
        return new CollectionAbstraction<>(collection);
    }
    static <T> Abstraction<T> of(T[] array) {
        return new ArrayAbstraction<>(array);
    }
    static IntArrayAbstraction of(int[] array) {
        return new IntArrayAbstraction(array);
    }
}

class CollectionAbstraction<T> implements Abstraction<T> {
    Collection<T> source;

    public CollectionAbstraction(Collection<T> source) {
        this.source = source;
    }

    @Override
    public void iterate(Consumer<? super T> action) {
        source.forEach(action);
    }
}

class ArrayAbstraction<T> implements Abstraction<T> {
    T[] source;

    public ArrayAbstraction(T[] source) {
        this.source = source;
    }

    @Override
    public void iterate(Consumer<? super T> action) {
        for (T t : source) {
            action.accept(t);
        }
    }
}

class IntArrayAbstraction implements Abstraction<Integer> {
    int[] source;

    public IntArrayAbstraction(int[] source) {
        this.source = source;
    }

    @Override
    public void iterate(Consumer<? super Integer> action) {
        for (int t : source) {
            action.accept(t);
        }
    }
}

class Test {
    public static void main(String[] args) {
        Abstraction.of(new Integer[] {1, 2, 3}).iterate(System.out::println);
        Abstraction.of(Arrays.asList(1, 2, 3)).iterate(System.out::println);
        Abstraction.of(new int[] {1, 2, 3}).iterate(System.out::println);
    }
}

I believe the approach above is pretty versatile. You don't depend on how a certain source is iterated, you may selectively modify them.

Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • Whereas this seems good practical advice for the OP, it does not directly respond to the actual question, which inquires about a common supertype of collections and arrays. – John Bollinger Apr 09 '19 at 12:05
  • 3
    @JohnBollinger I thought it was obvious from the comments that there is no neat solution, so I dropped that part. Now I elaborated on this in the last paragraphs. – Andrew Tobilko Apr 09 '19 at 14:16
  • 1
    The abstraction described in the second half of this answer is unnecessary, because something equivalent already exists in the standard library. Just replace `Abstraction` with `Collection`, and `Abstraction.of(T[])` with `Arrays.asList`. – David Apr 10 '19 at 00:33
  • @David what would you replace `Abstraction` with for arrays of primitives? – Andrew Tobilko Apr 10 '19 at 05:40
  • `Abstraction` doesn't work on arrays of primitives because `T` has to be a subclass of `Object`. Change `new Integer[]` to `new int[]` and it fails to compile. – John Kugelman Apr 10 '19 at 12:27
  • @JohnKugelman because it's an over-simplified abstraction ;) For instance, if you want to do it for `int[]`, you would need `IntConsumer` (not `Consumer `) [it's another hierarchy to cover all consumer classes], `T` would represent an abstract type (not a direct type of the source) [it's another hierarchy to include [or wrap] all the primitive types). It's quite complicated, so I decided to drop it. It's going to be a decent piece of code (similar to ones mapping frameworks usually have) – Andrew Tobilko Apr 10 '19 at 13:42
  • @JohnKugelman I updated the answer with `IntArrayAbstraction` to help you see how primitive arrays can be integrated into this (simplified) abstraction... – Andrew Tobilko Apr 10 '19 at 13:48
10

The other answers are all trying hard to answer the original title question:

Is there a common interface or superclass for arrays and collections?

But your real question is in the body:

Are there any other possibilities to handle both in one loop like the above?

The answer is: No, there's no way to write a single for loop that iterates over both collections and arrays.

You could jump through a bunch of hoops to turn the arrays into lists, but you'll almost certainly end up with a bigger mess than if you just wrote two (or more) loops. Calling getClass().isArray() tells you what you have but you still can't work with it without some sort of cast. Arrays.asList() doesn't work for arrays of primitives.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • Thank you for this response. It's a pity that you cannot abstract from Arrays/Collection and primitive/objects in Java, as the loop construction would remain the same and would make the code more readable. But I guess that's just the way it is and we have to live with it. – Pawel Os. Apr 10 '19 at 08:51
6

Depending on what you are trying to do, you might want to implement two similar methods:

public <T> void iterateOver(List<T> list) {
    // do whatever you want to do with your list
}

public <T> void iterateOver(T[] array) {
    this.iterateOver(Arrays.asList(array));
}

Or maybe even have an interface for this:

interface ExtendedIterableConsumer<T> {

    public void iterateOver(List<T> list);

    public default void iterateOver(T[] array) {
        this.iterateOver(Arrays.asList(array));

}

I am not sure if that helps you, because you seem to already have the object in question in a variable somewhere. But if you can address that problem one level higher, it might be useful.

glglgl
  • 89,107
  • 13
  • 149
  • 217
5

You can check if object is array by using isArray() method from Class

if (foo != null && (foo.getClass().isArray() || foo instanceof Collection<?>)){

}

Edit:

In terms of iterating over this foo object, there is no simple solution. However you could try something like this:

private void iterate(@NotNull Object foo) {
    if (foo instanceof Collection<?>) {
        for (Object o : ((Collection<?>) foo)) {
            chandleYourObject(o);
        }
    }

    if (foo.getClass().isArray()) {
        if (foo.getClass().isPrimitive()) {
            checkPrimitiveTypes(foo);
        }
        if (foo instanceof Object[]) {
            for (Object o : (Object[]) foo) {
                chandleYourObject(o);
            }
        }
    }
}

private void checkPrimitiveTypes(Object foo) {
    if (foo instanceof int[]) {
        for (int i : (int[]) foo) {

        }
    }
    //And the rest of primitive types
}

private void chandleYourObject(Object o ){
    //What you want to do with your object
}
Mershel
  • 542
  • 1
  • 9
  • 17
  • you can just check if `foo instanceof Collection>`. Shorter and more clear in my opinion. No nee to use `Collection.class.isAssignableFrom`. I've added partial answer because at first you didn't include `isArray()` method in your answer :) – Mershel Apr 09 '19 at 09:28
  • Though, I still believe that `Collection.class.isAssignableFrom(fooClass)` is more flexible than `foo instanceof Collection` because `Collection.class` could be any class at runtime (a dynamically obtained one, I mean) – Andrew Tobilko Apr 09 '19 at 09:46
  • Wouldn't that throw NPE when we would pass `int[] i = null`? I think that there will be no difference between `Collection.class.isAssignableFrom(fooClass)` and `foo instanceof Collection>`, that is just up to what you prefer and what is more readable for you ;) – Mershel Apr 09 '19 at 09:50
4

You could write a helper method for this:

@SuppressWarnings("unchecked")
public static <E> void forEach(Object arrayOrIterable, Consumer<? super E> action) {
    Objects.requireNonNull(arrayOrIterable);
    if (arrayOrIterable instanceof Iterable) {
        for (Object o : (Iterable<?>) arrayOrIterable) {
            action.accept((E) o);
        }
    } else if (arrayOrIterable.getClass().isArray()) {
        int length = Array.getLength(arrayOrIterable);
        for (int i = 0; i < length; i++) {
            action.accept((E) Array.get(arrayOrIterable, i));
        }
    } else {
        throw new IllegalArgumentException("not an array nor iterable: " + arrayOrIterable.getClass());
    }
}

The second branch makes use of the java.reflect.Array class which provides helper methods (may be slow), to get the length of an array and the element at a given index.

You may call it like this:

int[] ints = {1, 2, 3, 4};
forEach(ints, (Integer i) -> System.out.println(i));

List<Integer> ints = Arrays.asList(1, 2, 3, 4);
forEach(ints, (Integer i) -> System.out.println(i));

Due to the nature of generics, this method may throw a ClassCastException, e.g. this call:

int[] ints = {1, 2, 3, 4};
forEach(ints, (String s) -> System.out.println(s));

Would result in:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Lino
  • 19,604
  • 6
  • 47
  • 65
  • I wouldn't dare to name the parameter `arrayOrIterable` since it's an `Object` and it might be *anything* – Andrew Tobilko Apr 09 '19 at 08:59
  • 1
    @AndrewTobilko Well this method only works with an array or an iterable, and will throw an exception for every other `Object`, so this should help as documentation that really one of those two types is expected – Lino Apr 09 '19 at 09:02
  • oh, I get you. It's a matter of personal preference/style, I guess. – Andrew Tobilko Apr 09 '19 at 09:08
  • 2
    @AndrewTobilko It really is something that is purely opinion based, best would be of course an extensive java-doc stating what can be passed into this method, what exceptions may be thrown etc. – Lino Apr 09 '19 at 09:09
4

Arrays are Objects:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-10.html

AbstractCollections also extend Object:

https://docs.oracle.com/javase/8/docs/api/java/util/AbstractCollection.html

So yes there is a common superclass, but unfortunately this isn't really going to help you.

I would suggest your best bet is to use:

List<> someList = Arrays.asList(sourceArray)

This will convert your array into a collection that implements Iterable. You will of course need to work out if the initial object is an Array or a Collection in advance and only call the above if it is an array, here are some options for doing that:

boolean isArray = myArray.getClass().isArray();
boolean isCollection = Collection.class.isAssignableFrom(myList.getClass());
Ardesco
  • 7,281
  • 26
  • 49
  • 4
    `Arrays.asList()` will sadly not work with primitive arrays – Lino Apr 09 '19 at 09:06
  • 1
    Agreed, but I'm making the assumption here that the OP is not using primitive arrays. If they are they could do something like `Integer[] boxedInts = IntStream.of(ints).boxed().toArray(Integer[]::new);` (Assuming they aren't using a byte[] array, in which case it gets more complex) – Ardesco Apr 09 '19 at 09:26
  • 1
    `Arrays` is a utility class that "contains various methods for manipulating arrays". It is not the superclass of arrays. – Solomon Ucko Apr 09 '19 at 17:29
  • 1
    True, updated the answer to point towards Java documentation that states Arrays are Objects instead, – Ardesco Apr 09 '19 at 20:24