Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Array return can be used in assignment, but not in loop

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?

like image 427
Ben I. Avatar asked Jan 16 '15 17:01

Ben I.


Video Answer


1 Answers

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.

like image 122
rgettman Avatar answered Oct 25 '22 09:10

rgettman