14

I've been coding with C++ in school for 3 years now. I've started coding in Java just 2 days ago; my question is:

Is it bad practice to make generic arrays? What would be the alternative?

I am stumped and I can't seem to make a generic array besides doing something weird such as this example:

//Class implementing the MergeSort algorithm with generic types
// Revised by Doina January 2014

package Sorting;

import java.lang.*;

public class MergeSort {

    // Wrapper method for the real algorithm
    // T is the generic type which will be instantiated at runtime
    //  elementas are required to be comparable
    public static <T extends Comparable<T>> void sort(T[] a) {
        mergesort(a, 0, a.length - 1);
    }

    // Recursive mergesort method, following the pseudocode
    private static <T extends Comparable<T>> void mergesort(T[] a, int i, int j) {
        if (j - i < 1) return;
        int mid = (i + j) / 2;
        mergesort(a, i, mid);
        mergesort(a, mid + 1, j);
        merge(a, i, mid, j);
    }

    // Merge method
    // Here we need to allocate a new array, but Java does not allow allocating arrays of a generic type
    // As a work-around we allocate an array of type Object[] the use type casting
    // This would usually generate a warning, which is suppressed
    @SuppressWarnings("unchecked")
    private static <T extends Comparable<T>> void merge(T[] a, int p, int mid, int q) {

        Object[] tmp = new Object[q - p + 1];
        int i = p;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= q) {
            if (a[i].compareTo(a[j]) <= 0)
                tmp[k] = a[i++];
            else
                tmp[k] = a[j++];
            k++;
        }
        if (i <= mid && j > q) {
            while (i <= mid)
                tmp[k++] = a[i++];
        } else {
            while (j <= q)
                tmp[k++] = a[j++];
        }
        for (k = 0; k < tmp.length; k++) {
            a[k + p] = (T) (tmp[k]); // this is the line that woudl generate the warning
        }
    }

    // Main methos to test the code, using Integer Objects
    public static void main(String[] args) {
        Integer[] a = new Integer[5];
        a[0] = new Integer(2);
        a[1] = new Integer(1);
        a[2] = new Integer(4);
        a[3] = new Integer(3);
        a[4] = new Integer(-1);

        // T will be instantiated to Integer as a resutl of this call
        MergeSort.sort(a);

        // Print the result after the sorting
        for (int i = 0; i < a.length; i++)
            System.out.println(a[i].toString());
    }
}
Colonel Panic
  • 119,181
  • 74
  • 363
  • 435

3 Answers3

21

It's not that it's a bad idea per se; it's just that generics and arrays don't mix very well.

The reason is due to covariance and invariance. Arrays are covariant (Integer[] is an Object[] because Integer is an Object, but generic classes are invariant (List<Integer> is not a List<Object> even though an Integer is an Object).

You also have to deal with unchecked casts, which defeat the entire purpose of generics. The most common way to create a generic array - E[] foo = (E[]) new Object[10]; - is not type-safe and can't be enforced at compile time. It's possible to reason about it at runtime, but the compile-time checks which generics bring to the table are lost at that point.

To answer the question directly, where and when possible, you want to use Java Collections instead, as they play very nicely with generics.

Just glancing at your supplied code, I imagine that using List<T> instead of T[] would get you by most of your problems (and I would hope that you're passing an ArrayList in since those operations can become expensive with a linked list).

Makoto
  • 96,408
  • 24
  • 164
  • 210
  • 1
    ArrayList **does not** use generic arrays. [It uses an `Object[]`.](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/ArrayList.java#134) Also, `E[] foo = (E[]) new Object[10];` does not create a generic array; it is an erroneous cast that will be detected as soon as you try to assign the array to a non-generic variable, like `Integer[] a = Thing.methodReturningEArray()`. The only safe way to create a generic array is to use another array or a class object to get the type information at runtime, like `ArrayList.toArray(T[])` does. – user2357112 supports Monica Oct 16 '15 at 20:59
  • You're right about the `ArrayList` bit - I'll happily correct that, but I'm going to disagree with you on the second part, especially considering that there's a cast to `T[]` in that method. And if you go down [*that* rabbit hole](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/Arrays.java#Arrays.copyOf%28java.lang.Object%5B%5D%2Cint%2Cjava.lang.Class%29), you're casting `Object[]` to a generic `T[]` in a *lot* of places. So, those methods may be safe, but the convention I'm describing is used in the libraries, too. – Makoto Oct 16 '15 at 21:01
  • 1
    There's a cast to `T[]`, but that's because the array is known to actually be a `T[]`, as it was created using a `T[]` as a template. New-ing up an `Object[]` and casting it to a `T[]` is completely different. [Here, watch it fail!](http://ideone.com/sslO80) As for the "down the rabbit hole" example, it casts an `Object[]` to a `T[]` because it has just determined that `T` is, in fact, `Object`. – user2357112 supports Monica Oct 16 '15 at 21:08
  • Yes...you make a point there, but I don't think that what you're describing matches up cleanly with what I'm illustrating here. I even mention that there's no type-checking and that it's not safe at runtime because yes, *this very thing* can happen. – Makoto Oct 16 '15 at 21:15
  • 1
    You describe what you're doing as the "conventional (and only) way" to create a generic array when it isn't. It's more than just not typesafe; it's *already* a type error. It's simply not detected due to type erasure. If you want to talk about creating generic arrays in your answer, you should show examples like `ArrayList.toArray(T[])` and `Arrays.copyOf`, which do it right. – user2357112 supports Monica Oct 16 '15 at 21:32
  • 1
    While `(E[]) new Object[10]` fools the compiler into thinking the array is an `E[]`, the runtime knows it's an `Object[]`, which will cause a class cast exception as soon as a non-generic caller tries to use this array. So no, this is neither the conventional nor only way to create a generic array. – meriton Oct 17 '15 at 00:44
  • Is there a way the OP can require both `List` and `RandomAccess` interfaces? Would make it explicit they need it for the index access. – jpmc26 Oct 17 '15 at 05:12
4

It's not bad practice to create a generic array, but doing so correctly is so cumbersome people usually avoid it.

The reason it is cumbersome is that generics are erased, while arrays are reified. That is, type parameters are erased during compilation, while the component type of arrays is retained. Therefore, the runtime knows the component type of every array, but has forgotten the type arguments of all objects, i.e. the line

E[] array = new E[10];

does not compile because the runtime needs to know the component type for the new array, but has forgotten was E is.

The workaround in Makoto's answer:

E[] array = (E[]) new Object[10];

is not a good idea, as it actually creates an Object[], but then pretends to the compiler that is an E[]. As the the runtime has forgotten was E is, this cast also succeeds at runtime, even though it is not type correct. However, the runtime still enforces memory safety, by performing an additional check as soon as it is able, i.e. when the object is stored in a variable whose type is not generic. For instance:

static <E> E[] createArray(int size) {
    return (E[]) new Object[size];
}

public static void main(String[] args) {
    String[] array = createArray(size); // throws ClassCastException
    for (String s : array) {
        // whatever
    }
}

That is, this workaround is hack that only works under certain circumstances, and will cause highly puzzling behaviour otherwise (a ClassCastException in a line of code that does not contain a cast ...).

The only way to create an E[] is through reflection, by providing the class object of our desired component type:

Class<E> eClass = ...;
E[] array = Arrays.newInstance(eClass, 10);

but how can we get this class object? If our caller knows, they can pass us a class literal (like Integer.class), or we can use reflection on some other object. In your case, you have another E[] at hand, so you can ask that array what E is:

E[] originalArray = ...;
Class<E> eClass = (Class<E>) originalArray.getClass().getComponentType();
E[] newArray = (E[]) Array.newInstance(eClass, size);

This will ensure the new array is of the same type as the old one, which is E[], unless somebody lied to us about the type of that array using Makoto's workaround.

As you can see, it is possible to create a generic array, but it is so cumbersome that people usually go the great lengths to avoid it. The usual alternative are using an array of some super type (in your merge sort, Comparable[] might work even better than Object[], because you would not have to cast), or using an ArrayList instead.

meriton
  • 61,876
  • 13
  • 96
  • 163
1

Adding to Makoto's answer,I would say that Arrays are covariant due to the fact that their type information is available at runtime whereas generic classes are invariant as type information is not available due to type erasure at compile time.

Covariant Arrays :-

Object[] covariantArrays = new String[5];
covariantArrays[0] = new Dog(); // will give java.lang.ArrayStoreException

Invariant Arrays :-

List invariantArrays   = new List<String>();
invariantArrays.add(new Dog());   // Works fine as type information is not available

For this reason Generic arrays don't go well as Generics are limited to compile type safety and Arrays have real type information available even at Runtime

Kumar Abhinav
  • 6,274
  • 2
  • 19
  • 32