4

My goal here is to implement a method that will concatenate an arbitrary number of arrays into a single array of their common supertype, returning the resulting (typed) array. I have two implementations.

The first (this one doesn't need to be simplified):

public static <T> T[] concatArrays(Class<T> type, T[]... arrays) {
    int totalLen = 0;
    for (T[] arr: arrays) {
        totalLen += arr.length;
    }
    T[] all = (T[]) Array.newInstance(type, totalLen);
    int copied = 0;
    for (T[] arr: arrays) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return all;
}

Let's create some arrays:

Long[] l = { 1L, 2L, 3L };
Integer[] i = { 4, 5, 6 };
Double[] d = { 7., 8., 9. };

Our method is called with:

Number[] n = concatArrays(Number.class, l, i, d);

This works and is completely type-safe (e.g., concatArrays(Long.class, l, i, d) is a compiler error), but it's somewhat annoying to specify Number.class if it's not necessary. So I implemented the following method (this is the one I want to simplify):

public static <T> T[] arrayConcat(T[] arr0, T[]... rest) {
    Class commonSuperclass = arr0.getClass().getComponentType();
    int totalLen = arr0.length;
    for (T[] arr: rest) {
        totalLen += arr.length;
        Class compClass = arr.getClass().getComponentType();
        while (! commonSuperclass.isAssignableFrom(compClass)) {
            if (compClass.isAssignableFrom(commonSuperclass)) {
                commonSuperclass = compClass;
                break;
            }
            commonSuperclass = commonSuperclass.getSuperclass();
            compClass = compClass.getSuperclass();
        }
    }
    T[] all = (T[]) Array.newInstance(commonSuperclass, totalLen);
    int copied = arr0.length;
    System.arraycopy(arr0, 0, all, 0, copied);
    for (T[] arr: rest) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return all;
}

This is nicer to use from the client's perspective:

Number[] n = arrayConcat(l, i, d);

And again, the compiler is smart enough to give an appropriate error on Long[] all = arrayConcat(l, i, d). Since the compiler is able to recognize this error, it is clear that I am performing work at runtime (determining the common superclass of the given arrays) that the compiler is able to perform at compile time. Is there any way to implement my method without using my reflection-based method for determining a common superclass for the array creation step?

I tried this approach:

public static <T> T[] arrayConcat(T[]... arrays) {
    int totalLen = 0;
    for (T[] arr: arrays) {
        totalLen += arrays.length;
    }
    Object[] all = new Object[totalLen];
    int copied = 0;
    for (T[] arr: arrays) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return (T[]) all;
}

but this throws a ClassCastException upon returning. Obviously new T[totalLen] is also out. Does anyone have any other ideas?

Brandon
  • 2,367
  • 26
  • 32
  • I don't think you should be up casting here, I can understand putting same type arrays into one large array, but not mixing multiple types. – L7ColWinters Feb 28 '12 at 05:02
  • I can implement a simpler solution with that in mind (e.g., using `Arrays.copyOf(arr0, totalLen)` to create the initial array), but this leads to a new problem: now `Number[] n = arrayConcat(new Long[] { 1L, 2L, 3L }, new Integer[] { 4, 5, 6 }, new Double[] { 7., 8., 9. });` is *not* a compile-time error, but it results in an ArrayStoreException at runtime. – Brandon Feb 28 '12 at 05:15
  • Duplicate: http://stackoverflow.com/questions/80476/how-to-concatenate-two-arrays-in-java – Amir Afghani Feb 28 '12 at 06:01
  • Have you tried ClassName.arrayConcat(l, i, d). I think your ClassCastException is coming from the fact that T is being inferred to Long instead of Number. – Pradeep Gollakota Feb 28 '12 at 06:10
  • @Amir I assume you're referring to [this answer](http://stackoverflow.com/a/784842/1237044)? That actually fails as I mentioned in my comment above: `Number[] n = arrayConcat(new Long[] { 1L, 2L, 3L }, new Integer[] { 4, 5, 6 }, new Double[] { 7., 8., 9. });` is not a compile-time error, but it results in an ArrayStoreException at runtime. – Brandon Feb 28 '12 at 06:12
  • Really? It works for me? Which JDK are you using? – Amir Afghani Feb 28 '12 at 06:15
  • Also Brandon, did you try the answer directly above it? – Amir Afghani Feb 28 '12 at 06:16
  • My Java version is "Java(TM) SE Runtime Environment (build 1.6.0_26-b03)." The answer directly above the one I linked to is pseudocode... you can't declare an array of type T in Java. Let me clarify... my second method (marked "the one I want to simplify") works, but it seems like there has to be a better way to accomplish what I'm doing than to inspect each argument's type to find a common superclass at runtime. – Brandon Feb 28 '12 at 06:23
  • I don't think the generics are correct here. The passed in arguments are technically not of the type given by T. If we were to write the same code for lists instead of arrays, the method header would look like, `public static T[] arrayConcat(List extends T>... arrays)` I dont know how to say `Array extends T>`. It seems that, that's the real source of the issue. – Pradeep Gollakota Feb 28 '12 at 06:45

2 Answers2

9

You can do something like this:

public static <T> T[] arrayConcat(T[]... arrays) {
    int totalLen = 0;
    for (T[] arr: arrays) {
        totalLen += arr.length;
    }
    T[] all = (T[])Array.newInstance(
        arrays.getClass().getComponentType().getComponentType(), totalLen);
    int copied = 0;
    for (T[] arr: arrays) {
        System.arraycopy(arr, 0, all, copied, arr.length);
        copied += arr.length;
    }
    return all;
}

This takes advantage of the fact that when using varargs, the compiler constructs an array of the components, and the array's type is properly set up such that the component type is the vararg elements type. In this case the array has type T[][], so we can extract T and use it to construct our T[].

(One exception is if the caller calls it using a generic type as the varargs type, then the compiler can't construct the proper array type. However, if the caller does this it will generate a warning in the caller code (the infamous varargs generics warning), and so the caller is warned that bad things will happen, so it's not our fault.)

One amazing thing about this solution is that it does not produce the wrong answer even when the user passes zero arrays! (As long as the compiler compiles it successfully, it would have inferred (or been specified explicitly) some concrete type T such that T[] is a valid return type. That type T is given to us in the type of arrays) Apparently the compiler doesn't ever infer correctly in this case.

Note that a caller can manually pass the "arrays" argument, and in such case, it could have runtime type of U[][], where U is a subtype of T. In such a case, our method will return an array of runtime type U[], which is still a correct result, as U[] is a subtype of T[].

newacct
  • 119,665
  • 29
  • 163
  • 224
  • That's a wonderful answer! Very clever, exactly the kind of thing I was hoping for. Thanks! – Brandon Feb 28 '12 at 15:54
  • @BrandonMintern @newacct, I think you'll find it doesn't work with zero arguments. Take a look at this, for instance: http://pastebin.com/nV7pxxas . It fails at line 8 because the compiler infers a vararg array of `Object[]`, which it then tries to cast to `String[]` (causing a ClassCastException). javac also warns against the unchecked generic array there. – yshavit Feb 28 '12 at 21:51
  • @yshavit: yeah, I was afraid that it won't infer it for zero arguments. It then falls under the warning case. The caller would have to specify the type explicitly for zero arguments for it to call correctly then – newacct Feb 28 '12 at 22:43
  • @newacct: am I crazy, or should "totalLen += arrays.length" be "totalLen += arr.length" ? – Shorn Nov 28 '13 at 04:27
3

Simple answer: no. Although the type checker has done some work as far as inferring T, this information is erased by the time it gets to bytecode. Erasure is at the core of Java generics; understand it, and you'll understand generics.

Btw, generics and arrays don't mix well. If you try to concatenate a bunch of List<String>[]s into a single List<String>[], you're going to get compiler warnings (and general lack of type safety).

Your example #2 is actually doing more work at runtime than the compiler is able to do. Consider:

Number[] longs1 = new Long[] { 1L, 2L, 3L };
Number[] longs2 = new Long[] { 4L, 5L, 6L };
Number[] concatted = arrayConcat(longs1, longs2);

The compiler only knows that concatted is a Number[], but your method will (at runtime) figure out that the common type is actually Long[].

yshavit
  • 42,327
  • 7
  • 87
  • 124