When does a Java cast fail at compile time versus throwing ClassCastException at runtime?
answer
- Unrelated types → compile error
- Related types → compiles, may throw at runtime
- Compiler uses declared type; JVM checks actual type
- Upcast: compiles AND always safe
- Generics: unchecked cast warning, erasure defers the check
basics
~20 sIf two types are completely unrelated, the compiler rejects the cast immediately. If they are related (one could be the other) the cast compiles, but if the actual object is the wrong one it throws ClassCastException when the code runs.
solid answer
~50 sThe compiler reasons only about declared types. A cast between two types where neither can ever be the other — like (String) someAnimal — is a compile-time error, because it could never succeed. A cast between *related* types (a supertype to one of its subtypes, or across an interface the type might implement) is accepted by the compiler, since some object of that declared type might really be the target type. The compiler then defers the real check to runtime: the JVM compares the object's actual class against the cast type and throws ClassCastException if it doesn't match. So 'compiles' means 'the types are reconcilable in principle', not 'this cast is safe'. The takeaway: a green compile does not guarantee a cast succeeds; only an instanceof check or knowledge of the runtime type does. Interface casts are especially permissive because almost any class could implement a given interface.
go deeper
Knows some casts fail to compile and others fail when running, and that ClassCastException is the runtime one.
Articulates the declared-type vs actual-type rule and predicts which casts compile vs throw, including upcasts being always safe.
Explains why interface casts are permissive, the role of final, and how erasure makes generic casts unchecked, deferring failures to use sites.
Uses these rules to set API and lint policy: minimizing unchecked casts, justifying suppressions narrowly, and steering designs away from cast-dependent code paths.
## Two different gatekeepers A cast in Java passes through **two** checks, at two different times: 1. **Compile-time check** — performed by `javac` using only *declared* (static) types. 2. **Runtime check** — performed by the JVM using the object's *actual* (dynamic) type. Understanding which failures happen where is the whole topic. ## Compile-time rejection: provably impossible casts The compiler rejects a cast it can prove can **never** succeed for *any* object. For class types, that means neither type is a subtype of the other (they sit on unrelated branches of the hierarchy): ```java Animal a = new Dog(); String s = (String) a; // COMPILE ERROR: Animal and String are unrelated ``` There is no object that is both an `Animal` and a `String`, so the compiler refuses outright — `inconvertible types`. This is a *good* failure: caught early, free. ## Compile-time acceptance: reconcilable casts The compiler **accepts** a cast whenever *some* object could satisfy it. The two classic cases: ```java Animal a = ...; Dog d = (Dog) a; // OK to compile: a Dog IS-A Animal, so a *might* be a Dog ``` ```java Object o = ...; Comparable<?> c = (Comparable<?>) o; // OK: many classes implement Comparable ``` Downcasts (supertype → subtype) and casts to interfaces are accepted because the declared type leaves open the possibility. The compiler does **not** know the real object, so it cannot rule them out. ### Why interface casts are extra-permissive A non-`final` class could be extended by a subclass that implements *any* interface. So even `(SomeInterface) someConcreteObject` usually compiles, even when the concrete class doesn't implement it — the compiler can't prove a subclass won't. (A cast from a `final` class to an interface it doesn't implement *is* rejected, since no subclass can add it.) ## Runtime failure: ClassCastException For every cast the compiler *accepted but couldn't prove*, the JVM inserts a **checkcast** that runs when the line executes. It compares the object's actual class to the cast type: ```java Animal a = new Cat(); Dog d = (Dog) a; // compiles; at runtime: ClassCastException — actual class is Cat ``` The exception message names both types, e.g. `class Cat cannot be cast to class Dog`. ## Mental table | Relationship of declared & target types | Compiles? | Can throw at runtime? | |---|---|---| | Unrelated (neither is the other) | No (error) | n/a | | Target is a subtype (downcast) | Yes | Yes (if actual ≠ target) | | Target is a supertype (upcast) | Yes | No (always safe) | | Cast to an interface (non-final source) | Yes | Yes | ## Generics caveat: unchecked casts Because of **type erasure**, generic type arguments are not present at runtime. A cast like `(List<String>) someList` can only be checked down to the raw `List`, not the `<String>` part — the compiler warns *unchecked cast*, and a wrong element type surfaces later as a `ClassCastException` at the point of use, not at the cast. This is why such casts deserve scrutiny. ## Bottom line "It compiles" answers only *could these types ever be reconciled?* It does **not** answer *is this particular object the right type?* Only `instanceof` (or certainty about the runtime type) answers that.
- Why does casting an Object to an interface usually compile even if the object's class doesn't implement it?Because a non-final class could have a subclass that implements the interface, so the compiler can't prove the cast impossible. The actual check is deferred to runtime as a possible ClassCastException.
- Why does (List<String>) rawList only warn instead of fully checking?Type erasure removes the <String> argument at runtime, so the JVM can verify only that the object is a List, not its element type. The compiler emits an unchecked-cast warning, and a mismatch shows up later as a ClassCastException on element access.
saying these in an interview costs you the question
- Assuming a successful compile means the cast is safe.
- Thinking (String) animal compiles and throws at runtime — it is actually a compile error.
- Forgetting that interface casts almost always compile even when the concrete class doesn't implement the interface.
- Ignoring unchecked-cast warnings from generic casts under erasure.