4

I have a utility class:

public class ArrayUtils {
    public static <T> T[] concat(T[]... arrays) {
        if(arrays == null) {
            return null;
        }
        int size = 0;
        for(T[] array : arrays) {
            size += array.length;
        }

        T[] concatenatedArray = (T[]) new Object[size];

        int i = 0;
        for(T[] array : arrays) {
            for(T item : array) {
                concatenatedArray[i++] = item;
            }
        }

        return concatenatedArray;
    }
}

When I test concat, it crushes:

public class ArrayUtilsTest extends TestCase {

    public void testConcat() throws Exception {
        Integer[] first = new Integer[]{1,2,3};
        Integer[] second = new Integer[]{4,5,6};
        Integer[] concat = ArrayUtils.concat(first, second);
        assertEquals(6, concat.length);
    }
}

with message

java.lang.ClassCastException: java.lang.Object[] cannot be cast to java.lang.Integer[]

I guess it has to do with generics. Could you provide suggestions on making it work? Also, background on the issue would be great.

Thanks!

Konstantin Yovkov
  • 59,030
  • 8
  • 92
  • 140
midnight
  • 3,385
  • 3
  • 32
  • 57

5 Answers5

2

Your problem is that you are creating an Object array in your concat method.

Instead of

T[] concatenatedArray = (T[]) new Object[size];

use

T[] concatenatedArray = (T[]) Array.newInstance(arrays[0].getClass()
        .getComponentType(), size);

UPDATE: While the above will probably do the job for you, newacct pointed out in the comments that it is even better/safer to use

T[] concatenatedArray = (T[]) Array.newInstance(arrays.getClass()
        .getComponentType().getComponentType(), size);

This will allow you to skip the size check and use the method with e.g. ArrayUtils.concat(new String[]{"foo"}, new Integer[]{42}); (if needed), which otherwise would fail with an ArrayStoreException.

You can ignore the type safety warning, because you know that the array is of type T, but you should make sure that you have at least one array.

You could do this e.g. by modifying your method signature to

public static <T> T[] concat(T[] array1, T[]... arrays);

As a short explanation: This has to do with how generic types are implemented in java, cf. https://docs.oracle.com/javase/tutorial/java/generics/genMethods.html.

Actually your first cast in concat should fail as well (because in this case you are already trying to cast an Object[] to an Integer[]), but due to type erasure, the byte code will contain a cast to Object[] instead (you should be ably to verify this by looking at the byte code or using a decompiler).

On the other hand, your test method is not generic, but explicitly casts to Integer[] (even in the byte code ;)), so it will fail.

Last but not least as a general rule of thumb: If possible, try to use existing libraries like Apache Commons instead of reinventing the wheel. You will find that most of your problems were already solved in the past by someone else ;)

Marvin
  • 11,402
  • 2
  • 45
  • 48
  • Thanks. Would you mind elucidating why it doesn't work the old way? – midnight May 28 '15 at 11:01
  • @midnight You were essentially trying to do: `Integer[] concatenatedArray = (Integer[]) new Object[size];`, which is not possible. (an array of Object is not necessarily an array of Integer). If you try doing this without generics, you will have a compiler error. – Alderath May 28 '15 at 11:04
  • @midnight: I updated my answer. – Marvin May 28 '15 at 11:10
  • This will not work in general. `arrays[0]` could be of runtime class `U[]`, where `U` is a subtype of `T`, or `arrays[0]` could be `nil`, or `arrays` could be empty. So `arrays[0].getClass().getComponentType()` does not always get what you want. – newacct May 28 '15 at 18:59
  • @newacct: It works correctly for e.g. `concat(Number[], Integer[])` (which should be your first case), there was already a `NullPointerException` before if any of the arrays is `null` (which should be your second case) and I advised to modify the signature to guard against `arrays.size() == 0` (which should be your third case). What am I missing? – Marvin May 29 '15 at 08:30
  • @Marvin: Given your "instead of ... use ..." modification to the OP's code, this does not work: `ArrayUtils.concat(new String[]{"foo"}, new Integer[]{42});` – newacct May 29 '15 at 08:50
  • @newacct: Agreed and incorporated, thank you. Although I struggle to see the real world use case behind that combination ;) – Marvin Jun 01 '15 at 08:31
2

You can get the T from the actual arrays array-of-arrays parameter, like this:

T[] concatenatedArray = (T[]) Array.newInstance(arrays.getClass()
    .getComponentType().getComponentType(), size);
newacct
  • 110,405
  • 27
  • 152
  • 217
1

You may simplify your code by using what the JRE already offers:

public static <T> T[] concat(T[] first, T[]... arrays) {
    int size = first.length;
    for(T[] array : arrays) size += array.length;
    if(size==first.length) return first.clone();
    ArrayList<T> list=new ArrayList<>(size);
    Collections.addAll(list, first);
    for(T[] array: arrays) Collections.addAll(list, array);
    return list.toArray(first);
}

Calling toArray on a List providing an appropriately type array will create a new array of that type if the size is bigger than the size of the provided array. We protect against the special case, that the size is not bigger than the first array, which implies that all other arrays are empty, by checking for that case first and simply returning a clone of the first array then.

But in most cases you are better off using the List instead of dealing with arrays…

Holger
  • 243,335
  • 30
  • 362
  • 661
0

you have to specify the Class for which the Array is to be created

T[] concatenatedArray = (T[])Array.newInstance(arrays[0][0].getClass(), size);

This code will get the first array from the T[]... arrays and get the class type of the first element, which will be used to create the Final Array

Rahul Thachilath
  • 343
  • 3
  • 16
-1

public class ArrayUtilsTest extends TestCase {

public void testConcat() throws Exception {
    Integer[] first = new Integer[]{1,2,3};
    Integer[] second = new Integer[]{4,5,6};
//YOU need to cast the returned array from concat to an "Integer[]"
    Object[] a = ArrayUtils.concat(first, second);
    Integer[] concat = Arrays.copyOf(a, a.length, Integer[].class);

    assertEquals(6, concat.length);
}

}

Fudztown
  • 381
  • 2
  • 12