Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simplify this generic method to concatenate Java arrays

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?

like image 992
Brandon Avatar asked Sep 20 '25 23:09

Brandon


1 Answers

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

like image 137
newacct Avatar answered Sep 22 '25 12:09

newacct