public class Box<T> {
private T element;
public T getElement() {
return element;
}
public void setElement(T element) {
this.element = element;
}
}
public class Test {
public static void main(String[] args) {
List<Box> l = new ArrayList<>(); //Just List of Box with no specific type
Box<String> box1 = new Box<>();
box1.setElement("aa");
Box<Integer> box2 = new Box<>();
box2.setElement(10);
l.add(box1);
l.add(box2);
//Case 1
Box<Integer> b1 = l.get(0);
System.out.println(b1.getElement()); //why no error
//Case 2
Box<String> b2 = l.get(1);
System.out.println(b2.getElement()); //throws ClassCastException
}
}
The list l
holds element of type Box
. In case 1, I get the first element as Box<Integer>
and in second case the second element in the list is obtained as Box<String>
. The ClassCastException is not thrown in the first case.
When I tried to debug, the element's
type in b1
and b2
are String
and Integer
respectively.
Is it related to type erasure?
To be precise, the problem is PrintStream#println
.
Let's check the compiled code using javap -c Test.class
:
72: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
75: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
As you can see the compiler erased the types and also omitted a cast for Integer
, because it wasn't necessary here. The compiler already linked the used overloaded methoded to PrintStream#(Object)
.
It does that due to the JLS rule §5.3:
Method invocation conversion is applied to each argument value in a method or constructor invocation (§8.8.7.1, §15.9, §15.12): the type of the argument expression must be converted to the type of the corresponding parameter.
Method invocation contexts allow the use of one of the following:
The third rule is the conversion from a subtype to a supertype:
A widening reference conversion exists from any reference type S to any reference type T, provided S is a subtype (§4.10) of T.
And is done before the check if the type can be unboxed (the fifth check: "an unboxing conversion"). So the compiler checks that Integer
is a subtype of Object
and therefore it has to call #println(Object)
(your IDE will tell you the same if you check the called overloaded version).
The second version on the other hand:
95: invokevirtual #12 // Method blub/Box.getElement:()Ljava/lang/Object;
98: checkcast #14 // class java/lang/String
101: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
has a checkcast
to check of the retrieved type of Box#getElement
really is a String
. This is necessary, because your told the compiler it will be a String
(due to the generic type Box<String> b2 = l.get(1);
) and it linked the method PrintStream#(String)
. This check fails with the mentioned ClassCastException
, because an Integer
cannot be cast to String
.