- Exploring Generics Through a Generic Copy Method
- Arrays and Generics
- Exercises
- Conclusion
Arrays and Generics
In my comments following Listing 1 in Part 2, I mentioned that it wasn't possible to assign new E[size] to elements. Instead, I mentioned that (E[]) new Object[size]; would have to be assigned. Because of the way generics are implemented in Java, you cannot specify array-creation expressions that involve type parameters (such as new Set<E>[10]) or actual type arguments (such as new Map<String, Part>[100]).
Before we explore an example that demonstrates why it would be problematic to specify array-creation expressions that involve type parameters or actual type arguments, it's important to understand reification and covariance in the context of arrays, and erasure, which is at the heart of how generics are implemented.
Reification is the act of representing the abstract as if it was concrete[md]for example, making a memory address available for direct manipulation by other language constructs. Java arrays are reified. They're aware of their element types and can enforce these types at runtime. If you attempt to store an invalid element in an array, the Java runtime throws an instance of the ArrayStoreException class.
The following code fragment shows how an ArrayStoreException arises:
class Superclass { int a, b; } class Subclass extends Superclass { int c; } // Assume that the following code is located in the method of another class. Subclass[] subcArray = new Subclass[1]; Superclass[] supcArray = subcArray; supcArray[0] = new Superclass(); // ArrayStoreException thrown here
Let's examine the final three lines of code. The first of these lines (Subclass[] subcArray = new Subclass[1];) is acceptable at runtime because a compatible type is assigned to subcArray. In contrast, Subclass[] subcArray = (Subclass[]) new Superclass[1]; would result in a ClassCastException at runtime because the array knows that the assignment is illegal.
The second line (Superclass[] supcArray = subcArray;) is legal because of covariance (an array of supertype references is a supertype of an array of subtype references). In this case, an array of Superclass references is a supertype of an array of Subclass references. The nonarray analogy is that a subtype is also a supertype. For example, a String instance is a kind of Object instance.
Covariance can be dangerous when abused. For example, the third line (supcArray[0] = new Superclass();) results in an ArrayStoreException at runtime because a Superclass instance is not a Subclass instance. Without this exception, we might attempt to access the nonexistent member c and crash the virtual machine.
Unlike arrays, a generic type's type parameters are not reified. Because they're thrown away following compilation, they're not available at runtime. This "throwing away of type parameters" is one aspect of a process known as erasure, which also involves inserting casts to appropriate types when the code isn't type correct, and replacing type parameters by their upper bounds (such as Object).
Suppose it was possible to specify an array-creation expression that involves a type parameter or an actual type argument. Why would this be such a bad thing? For an answer to this question, consider the following code fragment, which should generate an ArrayStoreException instead of a ClassCastException but doesn't do so:
List<Country>[] cListArray = new ArrayList<Country>[1]; // Illegal, but assume otherwise. List<Integer> iList = new ArrayList<Integer>(); iList.add(25); Object[] oArray = cListArray; oArray[0] = iList; Country c = cListArray[0].get(0);
Let's assume that the first line is legal and creates a one-element array that's capable of storing a single List of Country element. The second line creates a List of Integer object. Thanks to autoboxing, the third line stores an Integer object containing 25 in the List of Integer object.
The fourth line assigns cListArray to oArray, which is legal because arrays are covariant and erasure converts List<Country>[] to the List[] runtime type. The fifth line assigns iList's reference to oArray[0], which is legal because erasure converts List<Integer> to the List runtime type, and List subtypes Object.
Thanks to erasure, the virtual machine doesn't throw ArrayStoreException in the fifth line. After all, we're assigning a List reference to a List[] array at runtime. However, this exception would be thrown if generic types were reified because we'd then be assigning a List<Integer> reference to a List<Country>[] array.
However, there's a problem. We've stored a List<Integer> instance in an array that can only store List<Country> instances. At runtime, when the compiler-inserted cast operator attempts to cast cListArray[0].get(0)'s return value (an Integer object containing 25) to Country, this operator throws an instance of the ClassCastException class.