Impossible Arrays
We cannot create an array of a generic type in Java. In the early days of the language, the Java designers decided that arrays could be subtypes of other array types, provided the elements themselves followed a subtype relationship. For example, assigning an Integer[]
to an Object[]
reference is perfectly legal.
Integer[]
is a subtype of Object[]
because arrays in Java are covariant on their element type. Covariance means that the subtype's element type may vary downwards in the hierarchy rooted on the supertype's element type. If you ask for a container of fruit, under covariance I can give you a box of oranges. We learned a moment ago that generic classes in Java are invariant, and now we are learning that arrays are covariant. This is an unfortunate inconsistency.
Assigning an Integer[]
to a String[]
reference is not legal because Integer
is not a subtype of String
.
The covariance of arrays introduces a problem. If an Integer[]
can be assigned to an Object[]
variable, the compiler can no longer detect that this code is dangerous:
items[0] = "Hydrogen";
The items
array is an array of Object
, and a String
is an Object
, so the compiler gives this assignment the thumbs-up. This is the exact same problem we encountered when trying to add PI
to a List<Integer>
. That problem was turned into a compilation error thanks to the invariance of generic classes. In this case, the assignment to Object[]
is allowed. Why the inconsistency? Java didn't initially have generics, so Object[]
became the go-to polymorphic array target used in library routines. For example, this universal method for comparing two arrays for equality is defined in the builtin Arrays
class:
class Arrays {
public static boolean equals(Object[] a, Object[] b) {
// ...
}
}
To ensure that clients could send their arrays to these library routines, the Java developers conceded that arrays of any non-primitive type could masquerade as Object[]
.
Nevertheless, the assignment of a String
to an array of Integer
cannot be allowed to succeed. Since the static type system can't detect the mismatch, the Java designers decided to add checks at runtime. Every time we construct an array, its element type is saved in hidden state. When an element is assigned, the Java virtual machine verifies that the new element is a subtype of the array's element type. If not, it throws an ArrayStoreException
. In this case, "Hydrogen"
is not an Integer
, and an exception is thrown.
A runtime exception is not quite as elegant as a compiler error, but it's better than nothing.
Now suppose we could make an array whose type depends on a generic parameter, like this array of ten string lists:
Object[] lists = new ArrayList<String>[10];
The type parameter String
is erased to Object
when this code is compiled. At runtime, then, the code behaves like this:
Object[] lists = new ArrayList[10];
The array's element type is just plain ArrayList
. Since the element type has been erased, there's not enough information to check that only ArrayList<String>
gets assigned to lists
. This code would not generate an ArrayStoreException
, even though it should:
lists[0] = new ArrayList<Integer>(); // uh oh
A language that allowed us to assign an ArrayList<Integer>
to an array of ArrayList<String>
would yield unsafe software. Programmers wouldn't trust the type system. The Java designers opted to preserve type safety by forbidding us from creating an array of generics.
As a workaround, we may allocate a raw array but assign it to a generic reference with a @SuppressWarnings
annotation:
Without the annotation, the compiler tries to warn us that this assignment isn't safe. Suppressing warnings is almost always a bad idea, but this code is in fact safe. The alternative of using an ArrayList
without a generic type is not.
The whole mess could have been avoided if generics had been in Java from the start and arrays had not been made covariant.