Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inconsistent ClassCastException thrown for Raw Types

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?

like image 247
Aman J Avatar asked Feb 05 '23 05:02

Aman J


2 Answers

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.

like image 114
Holger Avatar answered Feb 06 '23 18:02

Holger


It's because of the PrintStream#println:

public void println(Object x) {
    String s = String.valueOf(x);
    ...

See how it converts anything you give it to a String, but first assigning it to an Object (which works because Integer is an Object). Change your first code to:

    ArrayList xa = new ArrayList();
    xa.addAll(Arrays.asList("ASDASD", "B"));
    List<Integer> x = xa;
    Integer i = x.get(0);
    System.out.println(i);

and you will get the same failure.

EDIT

Yes, Didier is right in his comment; thus after thinking for a while the update.

This can the even simplified like this to understand why the compiler is inserting the extra checkcast #5 // class java/lang/Integer:

 ArrayList<Integer> l = new ArrayList<>();
 l.get(0).getClass();

At runtime the there's no Integer type, just plain Object; which would compile among other things to :

  10: invokevirtual #4 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
  13: checkcast     #5 // class java/lang/Integer
  16: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;

Notice the checkcast to check that the type that we get from that List is actually an Integer. List::get is a generic method, and that generic parameter at runtime would be an Object; to maintain the correct List<Integer> at runtime the checkcast is needed.

like image 39
Eugene Avatar answered Feb 06 '23 19:02

Eugene