I was posting an answer to a different question, when I came across a little mystery. The class definition (slightly modified from the original questioner) is here:
public class Playground<T>{
private int pos;
private final int size;
private T[] arrayOfItems;
public Playground(int size){
this.size = size;
pos = 0;
arrayOfItems = (T[]) new Object[size];
}
public void addItem(T item) {
arrayOfItems[pos] = item;
pos++;
}
public void displayItems() {
for(int i = 0;i<pos;i++){
System.out.println(arrayOfItems[i]);
}
}
public T[] returnItems() {
return (T[]) arrayOfItems;
}
}
In main, we then create a new Playground, Playground<String> animals = new Playground<String>(5);
and put some animal Strings in it. (Dog, Cat, etc).
The mystery is that this works:
Object[] s = animals.returnItems();
for(int i=0; i < s.length; i++) {
System.out.println(s[i]);
}
But this creates a ClassCastException
in the for loop declaration.
for(int i=0; i < animals.returnItems().length; i++) {
System.out.println(animals.returnItems()[i]);
}
Both Object[]
s and String[]
s have length variables. Why does using the accessor method in the loop declaration cause an exception?
The reason that there is a ClassCastException
-- cannot cast from Object[]
to String[]
-- is because of what the compiler does when using generics. When calling returnItems()
, the compiler inserts a cast to String[]
, because returnItems
returns T[]
. Type erasure upon compilation means that it's returning an Object[]
, but since T
is String
here, the compiler inserts a cast to String[]
. But the original object arrayOfItems
isn't a String[]
, it's an Object[]
, so the cast fails.
This should have resulted in an "unchecked cast" warning during compilation, from Object[]
to T[]
.
What you will need to do instead is to follow the advice here in How to create a generic array in Java? in creating a generic array.
Accept a Class<T>
in your constructor, so you can call Array.newInstance
and get a T[]
from the start.
@SuppressWarnings("unchecked") // This suppression is safe.
public Playground(int size, Class<T> clazz){
this.size = size;
pos = 0;
arrayOfItems = (T[]) Array.newInstance(clazz, size);
}
Then you can create animals
by passing String.class
:
Playground<String> animals = new Playground<String>(5, String.class);
Update
The following is a reasonable explanation as to why the first example works (assigning to an Object[]
) when the second example doesn't work (accessing the field length
directly on the return type of the returnItems()
method.
First example
Object[] s = animals.returnItems();
for(int i=0; i < s.length;i++) {
System.out.println(s[i]);
}
The JLS, Section 5.2, describes "Assignment Contexts" which govern what happens when assigning a value from an expression to a variable.
The only exceptions that may arise from conversions in an assignment context are:
- A ClassCastException if, after the conversions above have been applied, the resulting value is an object which is not an instance of a subclass or subinterface of the erasure (§4.6) of the type of the variable.
This circumstance can only arise as a result of heap pollution (§4.12.2). In practice, implementations need only perform casts when accessing a field or method of an object of parameterized type when the erased type of the field, or the erased return type of the method, differ from its unerased type.
...
The compiler did not need to insert a cast to String[]
here. When the length
field is accessed later, the variable is already of type Object[]
, so there is no problem here.
Second example
for(int i=0; i < animals.returnItems().length;i++) {
System.out.println(animals.returnItems()[i]);
}
The ClassCastException
here appears not to be dependent on the for
loop; this error will occur with a simple print of the length:
System.out.println(animals.returnItems().length);
This is a field access expression, covered by the JLS, Section 15.11.1.
[T]he identifier names a single accessible member field in type T, and the type of the field access expression is the type of the member field after capture conversion (§5.1.10).
Capture conversion captures the type as String[]
. The compiler must insert the cast to String[]
here for the same reason as it would insert the cast for a method call - the field or method may only exist on the captured type.
Because the type of arrayOfItems
is really Object[]
, the cast fails.
As described above, creating the generic array with Array.newInstance
solves this issue, because an actual String[]
is being created. With that change, the inserted cast is still present, but this time it succeeds.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With