Matthieu Matthieu - 5 months ago 20
Java Question

How does the JVM actually cast objects and issue a ClassCastException?

What happens under the hood when you cast an object to a specific class like

casted = (NewClass)obj;
? I'm guessing the JVM somehow checks if the actual class of
obj
is a subclass of
NewClass
, but would there be a way for an object instance to know when it is being "casted"?

Pointers to the documentation/FAQ of some JVM implementations are also welcome, as I haven't been able to find any...

Answer

The JVM has a bytecode, checkcast, which is used to check if a cast can be validly performed. The actual cast check semantics are described in the JLS§5.5.3, and the details of the checkcast bytecode are described in the JVM spec§6.5. As an example,

public static void main(String args[]) {
   Number n = Integer.valueOf(66); // Autoboxing

   incr((Integer) n);

   System.out.println(n);
}

produces

 public static void main(java.lang.String[]);
    Code:
       0: bipush        66
       2: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: aload_1
       7: checkcast     #4                  // class java/lang/Integer
      10: invokestatic  #5                  // Method incr:(Ljava/lang/Integer;)V
      13: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: aload_1
      17: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      20: return

Additionally, by delving into Hotspot's source code, bytecodeInterpreter.cpp line 2048 we can actually see what happens in the bytecode interpreter when a checkcast is reached:

  CASE(_checkcast):
      if (STACK_OBJECT(-1) != NULL) {
        VERIFY_OOP(STACK_OBJECT(-1));
        u2 index = Bytes::get_Java_u2(pc+1);
        if (ProfileInterpreter) {
          // needs Profile_checkcast QQQ
          ShouldNotReachHere();
        }
        // Constant pool may have actual klass or unresolved klass. If it is
        // unresolved we must resolve it
        if (METHOD->constants()->tag_at(index).is_unresolved_klass()) {
          CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception);
        }
        Klass* klassOf = (Klass*) METHOD->constants()->slot_at(index).get_klass();
        Klass* objKlassOop = STACK_OBJECT(-1)->klass(); //ebx
        //
        // Check for compatibilty. This check must not GC!!
        // Seems way more expensive now that we must dispatch
        //
        if (objKlassOop != klassOf &&
            !objKlassOop->is_subtype_of(klassOf)) {
          ResourceMark rm(THREAD);
          const char* objName = objKlassOop->external_name();
          const char* klassName = klassOf->external_name();
          char* message = SharedRuntime::generate_class_cast_message(
            objName, klassName);
          VM_JAVA_ERROR(vmSymbols::java_lang_ClassCastException(), message);
        }
      } else {
        if (UncommonNullCast) {
            //              istate->method()->set_null_cast_seen();
            // [RGV] Not sure what to do here!

        }
      }
      UPDATE_PC_AND_CONTINUE(3);

In a nutshell, it grabs the argument off the stack, gets the Class object from the constant pool (resolving if necessary), and checks if the argument is assignable to that class. If not, it gets the names of the object's type and of the class to which the cast was attempted, constructs an exception message, and throws a ClassCastException with that message.