When executing the below code, the code is executed perfectly without any errors, but for a variable of type List<Integer>
, the return type of get()
method should be Integer, but while executing this code, when I call x.get(0)
a string is returned, whereas this should throw an exception.
public static void main(String[] args)
{
ArrayList xa = new ArrayList();
xa.addAll(Arrays.asList("ASDASD", "B"));
List<Integer> x = xa;
System.out.println(x.get(0));
}
But while executing the below code, just adding the retrieval of class from the returned object to the previous code block throws a class cast exception. If the above code executes perfectly the following should also execute without any exception:
public static void main(String[] args)
{
ArrayList xa = new ArrayList();
xa.addAll(Arrays.asList("ASDASD", "B"));
List<Integer> x = xa;
System.out.println(x.get(0).getClass());
}
Why does java execute a type conversion while fetching the class type of the object?
The compiler has to insert type checking instructions at the byte code level where necessary, so while an assignment to Object
, e.g. Object o = x.get(0);
or System.out.println(x.get(0));
, may not require it, invoking a method on the expression x.get(0)
does require it.
The reason lies in the binary compatibility rules. Simply said, it is irrelevant whether the invoked method has been inherited or explicitly declared by the receiver type, the formal type of the expression x.get(0)
is Integer
and you are invoking the method getClass()
on it, hence, the invocation will be encoded as an invocation of a method named getClass
with the signature () → java.lang.Class
on the receiver class java.lang.Integer
. The facts that this method has been inherited from java.lang.Object
and that it was declared final
at compile time, are not reflected by the compiled class.
So in theory, at runtime, the method could have been removed from java.lang.Object
and a new method java.lang.Class getClass()
added to java.lang.Integer
without breaking the compatibility to that specific code. While we know that this will never happen, the compiler is just following the formal rules not to inject assumptions about the inheritance into the code.
Since the invocation will be compiled as an invocation targeting java.lang.Integer
, a type cast is necessary before the invocation instruction, which will fail in the Heap Pollution scenario.
Note that if you change the code to
System.out.println(((Object)x.get(0)).getClass());
you will make the assumption explicit that the method has been declared in java.lang.Object
. The widening to java.lang.Object
will not generate any additional byte code instruction, all this code does, is changing method invocation’s receiver type to java.lang.Object
, eliminating the need for a type cast.
There is an interesting deviation from the rules here, that the compiler does encode the invocation as an invocation on java.lang.Object
on the bytecode level, if the method is one of the known final
methods declared in java.lang.Object
. This might be due to the fact that these specific method are specified in the JLS and encoding them in this form allows the JVM to identify these special methods quickly. But the combination of the checkcast
instruction and the invokevirtual
instruction still exhibits the same, compatible behavior.