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?
Ideone link
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:
- an identity conversion (§5.1.1)
- a widening primitive conversion (§5.1.2)
- a widening reference conversion (§5.1.5)
- a boxing conversion (§5.1.7) optionally followed by widening reference conversion
- an unboxing conversion (§5.1.8) optionally followed by a widening primitive conversion.
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
.
Ok, the problem here is that b2 is incorrectly marked as being Box<String>
when it's actually Box<Integer>
(the type of box2) - so b2.getElement()
is typed as String, even though it actually contains an Integer. The compiler tries to call the overloaded println method which takes a String rather than the method which takes an Object and so you get a ClassCastException. The Object version of println does an explicit conversion of its argument to a String (via a call to toString()) but the String version of the method doesn't do that.
The underlying problem is using raw types rather than fully specifying the type parameter for list l
- it should have been List<Box<?>>
. Then you'd have had b1
and b2
as Box and the right overload of System.out.println would have been chosen.
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