2

Can anyone explain to me why this happens:

public class Array<E> {

    public E[] elements = (E[]) new Object[10];

    public E get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.
    }

    public static void doSomething(Array<Integer> arr) {
        Integer good = arr.get(0);
        Integer bad1 = arr.elements[0];
        Integer bad2 = ((Integer[]) arr.elements)[0];
        Integer bad3 = (Integer) arr.elements[0];
        // `bad1', `bad2', and `bad3' all produce a 
        // runtime exception.
    }

    public static void main(String[] args) {
        Array<Integer> test = new Array<>();

        Array.doSomething(test);
    }
}

Full example here: http://pastebin.com/et7sGLGW

I've read about type erasure and realize type-checking is performed during compilation and then E is simply replaced with Object, so all we have is public Object[] elements, but why does the get method succeed where regular type-casting doesn't? Doesn't the get's method return type also get erased?

Thanks.

d125q
  • 1,491
  • 10
  • 17
  • I don't get a runtime exception, but I had to add extra code to get things to compile and create `elements`, and it's probably not the same as your code. I think we need to see a more complete example. – ajb Oct 15 '14 at 20:44
  • 1
    Generics and arrays do not work together in Java unfortunately – hoaz Oct 15 '14 at 20:45
  • 1
    @hoaz Certain combinations of generics and arrays are illegal in Java, and the compiler will flag an error. But if the program compiles, I think the program should run as expected, but the questioner is saying there's a runtime exception. However, I couldn't get one. – ajb Oct 15 '14 at 20:47
  • This is even better: `Object o = arr.elements[0];` results in ` java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer`. This tells me that the cast that fails is the original `Object[]` to `Integer[]` cast! –  Oct 15 '14 at 21:04
  • @ajb: Here's some code: http://pastebin.com/et7sGLGW – d125q Oct 15 '14 at 21:06

5 Answers5

3

Even though arr has type Array<Integer> (and arr.elements has type Integer[]), arr.elements actually has runtime-type Object[], because the actual array is an instance of type Object[].

(Note that arrays, unlike generics, are covariant, and do not have erasure. Object[] foo = new String[5]; is legal — as is String[] bar = (String[]) foo;. Whereas Integer[] baz = (Integer[]) foo; would raise a ClassCastException at runtime.)

So the reason that any reference to arr.elements triggers a runtime-exception is that the compiler automatically inserts a downcast to Integer[], to bring the type and the runtime-type back into accord. Inside the body of doSomething, arr.elements actually means (Integer[]) arr.elements, with an implicit cast.

By contrast, inside get(), the type of this is just Array<E>, so the type of elements is just E[], which cannot be checked. So the compiler does not insert any implicit casts.


The major take-home point is that (E[]) new Object[10]; is actually incorrect. new Object[10] does not create an instance of E[]. The compiler can't see that it's incorrect, but it will insert lots of casts that will see that it's incorrect.

A better approach is to use Object[] elements = new Object[], and to perform correct-but-unchecked casts from Object to E, when necessary, rather than incorrect-and-unchecked casts from Object[] to E[].

Do you see what I mean?

ruakh
  • 156,364
  • 23
  • 244
  • 282
2

NOTE: This answer refers to the code at your pastebin link. I recommend that you edit your question and include the entire code. It's not that long.

The problem in this particular code is that you declared the doSomething parameter arr to be of type Array<Integer>. So when you say

    Integer bad1 = arr.elements[0];

Since arr is Array<Integer>, i.e. the type parameter E is Integer, the compiler assumes the type of elements, which was declared as E[], is an Integer[].

However, when elements is created with new, either in the constructor or in append where you create it as temp, you created it as Object[]:

elements = (E[]) new Object[(capacity = 2)];

or

E[] temp = (E[]) new Object[(capacity *= 2)];
...
elements = temp;

This means that when the array object is created at runtime, its type will be recorded, at runtime, as Object[]. The cast to E[] does not affect this, since the runtime type of an object never changes. A cast only affects how the compiler looks at an expression.

Therefore, in this statement:

Integer bad1 = arr.elements[0];

Here, the compiler "knows" that elements is supposed to be of type Integer[], as explained above. Since the actual runtime type of elements is Object[], and Object[] cannot be implicitly cast to Integer[], a ClassCastException occurs at runtime:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

([L means array.)

The reason this doesn't occur when you use get() is that the code that accesses the array isn't in a place where the compiler "knows" that E is supposed to be Integer. Therefore, it doesn't assume that elements is an Integer[], and no checking is done.

(I'm not entirely certain about the exact mechanisms involved with generics, but I think this is right.)

ajb
  • 29,914
  • 3
  • 49
  • 73
2

First, you have an Object[] and you're making it pass as E[] by using

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

This is the culprit of all the problems.

Why is this a problem?

  1. You cannot even create such array. It will compile with no problems, but theorically (and at runtime) this is invalid. Here's an example with Integer[]:

    Integer[] stringArray = (Integer[])new Object[10];
    

    This line throws an error at runtime:

    java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    But generics hides this problems until you have to use the data directly.

  2. Let's try this piece of code:

    public class Array<E> {
        public E[] elements = (E[]) new Object[10];
    }
    
    public class Client {
        public static void main(String[] args) {
            Array<Integer> array = new Array<>();
            array.elements[0] = 5;
        }
    }
    

    The whole code compiles with no problems, and the initialization of array works as expected. But now we get a new exception:

    array.elements[0] = 5;
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    Which means that internally we're still working with an Object[] but trying to make it work as Integer[], or more formally, as E[] (which is wrong as stated in 1.).

  3. Let's add an append method (adapted from code posted by OP):

    public class Array<E> {
        public E[] elements = (E[]) new Object[10];
        private int size = 0;
        public void append(E element) {
            elements[size++] = element;
        }
    }
    
    public class Client {
        public static void main(String[] args) {
            Array<Integer> array = new Array<>();
            array.append(5);
            System.out.println(array.elements[0]);
        }
    }
    

    Here, we will obtain the runtime error here:

    System.out.println(array.elements[0]);
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    Due to the same reasons explained above. Note that this will be the case for your three examples in your post.

In short:

You should not use E[] array directly. Instead, use Object[] array as described in ArrayList source code.

More info:

Community
  • 1
  • 1
Luiggi Mendoza
  • 81,685
  • 14
  • 140
  • 306
2

Type erasure removes the generic types from your generic class, but it will insert type casts where you are using the generic type if necessary. In your example there are type casts added by the compiler when you use the generic Array class. However inside the generic Array occurences of E decay to Object. (see comments and output of javap). The error you see is then simply the compiler complaining about a cast from Object[] to Integer[], which is illegal (generics or not).

public class Array<E> {
    public E[] elements;
    @SuppressWarnings("unchecked")
    public Array() {
        this.elements = (E[])new Object[]{1,2,3};
    }
    public E get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.                                                        
    }

    public static void doSomething(Array<Integer> arr) {
        Integer good = arr.get(0);                             // produces (Integer) arr.get(0)                            
        Integer good1 = (Integer) ((Object[])arr.elements)[0]; // no implicit cast                                         
        Integer bad1 = arr.elements[0];                        // produces ((Integer[])arr.elements)[0]                    
        Integer bad2 = ((Integer[]) arr.elements)[0];          // produces ((Integer[])((Integer[])arr.elements))[0]       
        Integer bad3 = (Integer) arr.elements[0];              // produces ((Integer[])arr.elements)[0]                    
        // `bad1', `bad2', and `bad3' all produce a                                                                        
        // runtime exception.                                                                                              
    }

    public static void main(String[] args) throws Exception{
        doSomething(new Array<Integer>());
    }
}

Output of javap -cp . -c Array

> public static void
> doSomething(Array<java.lang.Integer>);
>     Code:
>        0: aload_0       
>        1: iconst_0      
>        2: invokevirtual #6                  // Method get:(I)Ljava/lang/Object;
>        5: checkcast     #7                  // class java/lang/Integer
>        8: astore_1      
>        9: aload_0       
>       10: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       13: checkcast     #4                  // class "[Ljava/lang/Object;"
>       16: iconst_0      
>       17: aaload        
>       18: checkcast     #7                  // class java/lang/Integer
>       21: astore_2      
>       22: aload_0       
>       23: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       26: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       29: iconst_0      
>       30: aaload        
>       31: astore_3      
>       32: aload_0       
>       33: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       36: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       39: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       42: iconst_0      
>       43: aaload        
>       44: astore        4
>       46: aload_0       
>       47: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       50: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       53: iconst_0      
>       54: aaload        
>       55: astore        5
>       57: return
revau.lt
  • 2,404
  • 2
  • 15
  • 29
1

It is instructive to consider what your code looks like after type erasure, because this shows all the casts explicitly:

public class Array {

    public Object[] elements = new Object[10];

    public Object get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.
    }

    public static void doSomething(Array arr) {
        Integer good = (Integer)arr.get(0);
        Integer bad1 = ((Integer[])arr.elements)[0];
        Integer bad2 = ((Integer[]) arr.elements)[0];
        Integer bad3 = (Integer) ((Integer[])arr.elements)[0];
        // `bad1', `bad2', and `bad3' all produce a 
        // runtime exception.
    }

    public static void main(String[] args) {
        Array test = new Array();

        Array.doSomething(test);
    }
}

With this, it's obvious why the cast exceptions happen when they do.

You might ask, why do the casts happen when they do. Why is arr.elements cast to Integer[]? It's because after type erasure, arr.elements has a type of Object[]; but in the method, we are using it and expecting it to be Integer[], so a cast is necessary when it exits the generic scope and enters the method where we have a specific type substituted for T.

This cast is not necessarily always made, if arr.elements is immediately passed or assigned into something that expects type Object[] or Object, then the cast is not made because it is unnecessary. But in other cases, the cast is made.

Arguably, this would not be an incorrect type erasure of the code:

Integer bad1 = (Integer)arr.elements[0];

And a compiler presumably could have compiled it like this if was really smart and it really wanted to. But to do this, the compiler would need to add an additional complex rule about whether to cast things coming out of generics, to figure out whether it can defer it until after an array access. Plus this would have no benefit from the compiler's point of view, because there is still one cast either way. So the compiler does not do it this way.

newacct
  • 110,405
  • 27
  • 152
  • 217