12

First Code:

List<Integer>[] array = (List<Integer>[]) new Object[size]; 

It will give the following exception:

java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.util.List; ([Ljava.lang.Object; and [Ljava.util.List; are in module java.base of loader 'bootstrap')

Why is this wrong? I just follow the Effective Java Third Edition Page 132's method:

Second Code:

E[] array = (E[]) new Object[size];

However I found the following code works

Third Code:

List<Integer>[] array = (List<Integer>[]) new List[size];

My questions:

  1. Why first code is wrong but second code is suggested in Effective Java? Is there something I misunderstand?

For example: why the following code works well, but first code is wrong?

public class Test<E>{
    E[] array;
    public Test(){
        array = (E[]) new Object[10];
    }
    public E set(E x){
        array[0] = x;
        System.out.println(array[0]);
        return array[0];
    }

    public static void main(String[] args){
        Test<List<Integer>> test = new Test<>();
        List<Integer> list = new ArrayList<>();
        list.add(1);
        test.set(list);
    }
}
  1. Can anyone explain why third code is correct but the following code is wrong?

Fourth Code:

List<Integer>[] array = new List<Integer>[size];

maplemaple
  • 1,297
  • 6
  • 24
  • 1
    Please include the compiler error. Also, mixing generics and arrays is almost always trouble since arrays are covariant and retained while generics are invariant and erased. – Turing85 Jul 12 '19 at 06:15
  • You have to be very careful with downcasting. Not every Object is a List, but every List is an Object. Check this, https://stackoverflow.com/a/24366779/4961874 – Farhan Farooqui Jul 12 '19 at 06:24
  • @FarhanFarooqui I've updated my question. How to explain my question 1? – maplemaple Jul 12 '19 at 07:11
  • `List[] array = (List[]) new List[size];` this works? you sure? this will not even compile – Eugene Jul 12 '19 at 07:14
  • @Eugene I said first code wrong, but 2nd code correct. You can paste the code below the question 1 to your ide. It works well. – maplemaple Jul 12 '19 at 07:23

3 Answers3

9

First Code

List<Integer>[] array = (List<Integer>[]) new Object[size]; 

The reason why the first code fails is because casting does not change the actual type of the array, it just makes the compiler accept the code as valid. Imagine if you had another reference to the underlying object array:

final int size = 2;
Object[] objectArr = new Object[size];
List<Integer>[] integerArr = (List<Integer>[]) objectArr; // Does not work
objectArr[0] = "foobar";
List<Integer> i = integerArr[0]; // What would happen ??

The above code compiles fine, since you are forcing the compiler to accept it with the cast. But you can already see why it would be a problem for the cast to work at runtime: you would end up with an List<Integer>[] that now contains a String, which makes no sense. So the language disallows this.

Second Code

E[] array = (E[]) new Object[size];

Generics in Java are kind of odd. For various reasons, such as backwards compatibility, generics are basically erased by the compiler and will (mostly) not appear in the compiled code (Type Erasure). Instead, it will use a series of rules (JLS spec) to determine what type should be used in the code instead. For a basic unbouded generic; this type will be Object. So, assuming there is no bound on E, the second code is changed by the compiler to this:

 Object[] array = (Object[]) new Object[size];

So since both arrays have the exact same type after the erasure, there is no problem at runtime and the cast is basically redundant.

It is worth noting that this only works as long as E is unbounded. For example, this will fail at runtime with a ClassCastException:

public static <E extends Number> void genericMethod() {
    final int size = 5;
    E[] e = (E[]) new Object[size];
}

That is because E will be erased to Number, and you will get the same problem as the first code:

Number[] e = (Number[]) new Object[size];

It is important to keep the erasure in mind when working with code. Otherwise you may run into situations where the code acts different from what you expect. For example, the following code compiles and runs without exceptions:

public static <E> void genericMethod(E e) {
    final int size = 2;
    Object[] objectArr = new Object[size];
    objectArr[0] = "foobar";

    @SuppressWarnings("unchecked")
    E[] integerArr = (E[]) objectArr;
    integerArr[1] = e;

    System.out.println(Arrays.toString(integerArr));
    System.out.println(e.getClass().getName());
    System.out.println(integerArr.getClass().getName());
}

public static void main(String[] args) {
    genericMethod(new Integer(5)); // E is Integer in this case
}

Third Code

List<Integer>[] array = (List<Integer>[]) new ArrayList[size];

Similarly to the case above, the third code will be erased to the following:

 List[] array = (List[]) new ArrayList[size];

Which is no problem because ArrayList is a subtype of List.

Fourth Code

List<Integer>[] array = new ArrayList<Integer>[size];

The above will not compile. The creation of arrays with a type that has a generic type parameter is explicitely disallowed by the spec:

It is a compile-time error if the component type of the array being initialized is not reifiable (§4.7).

A type with a generic parameter that is not an unbounded wildcard (?) does not satisfy any condition for reifiability:

A type is reifiable if and only if one of the following holds:

  • It refers to a non-generic class or interface type declaration.
  • It is a parameterized type in which all type arguments are unbounded wildcards (§4.5.1).
  • It is a raw type (§4.8).
  • It is a primitive type (§4.2).
  • It is an array type (§10.1) whose element type is reifiable.
  • It is a nested type where, for each type T separated by a ".", T itself is reifiable.
TiiJ7
  • 3,332
  • 5
  • 25
  • 37
  • +1 for the fact that you mentioned type erasure + bounded type erasure. though why the cast fails or not is still in the JLS, under a different name... – Eugene Jul 12 '19 at 10:06
4

Though I don't have time to dig very deep in the JLS, I can hint you were to look further (though every time I do this, it ain't a very pleasant trip).

List<Integer>[] array = (List<Integer>[]) new Object[size]; 

this does not compile because these are provably distinct types (search the JLS for such a notion). In simpler words, the compiler is "able" to see these types can not possibly be of same type that can potentially be casted, thus fails.

On the other hand:

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

these are not a provably distinct types; the compiler can't tell for a fact that this has to fail. The slightly other thing here, is that casting to a generic type is not enforced by the compiler in no form or shape, you could have easily do something like this (that would still compile):

String s[][][] = new String[1][2][3];
array = (E[]) s; // this will compile, but makes little sense 

The second point is type erasure (again JLS has it).

After you compile the code, E[], at runtime, is Object[] (unless there is a bound, but not the case here), well you can obviously put anything you want into that.

Eugene
  • 117,005
  • 15
  • 201
  • 306
0

Interaction between arrays and generics in Java is messy, becuase they are built on different assumptions. Java arrays have run time type checking, generics only have compile time type checking.

Java implements type-safety through a combination of compile-time and runtime checks. Casting bypasses most of the compile-time checks, but there are still run-time checks. Arrays have essentially the same type-compatibility rules as their element types they contain. So:

Object[] a = new String[size]; //ok, but be aware of the potential for an ArrayStoreException
String[] a = new Object[size]; //compile error
String[] a = (String[]) new Object[size]; //runtime error

When Sun decided to add generics to Java they decided that code using generics should work on existing JVMs, as a result they decided to implement generics through erasure. Generic types only exist at compile time, at run time they are replaced by plain types.

So before erasure we have the following statements.

List<Integer>[] array = (List<Integer>[]) new Object[size];
E[] array = (E[]) new Object[size];
List<Integer>[] array = (List<Integer>[]) new List[size];

After erasure we have.

List[] array = (List[]) new Object[size]; //run time error.
Object[] array = (Object[]) new Object[size]; //no error.
List[] array = (List[]) new List[size]; //no error.

The E[] array = (E[]) new Object[size]; structure should be used with caution, it's a violation of the normal typing model of Java and will cause a confusing ClassCastException if the array is ever returned to a non-generic context. Unfortunately there is often no better option, because of type erasure there is no way for a generic type to find out it's element type and construct an array of the correct type.

plugwash
  • 9,724
  • 2
  • 38
  • 51