Why can the Java compiler accept some casts that fail at runtime, but reject others outright as compile errors?
answer
- Compiler rejects only provably-impossible casts (inconvertible types)
- Plausible = same inheritance line, or an interface a subclass could implement
- Interfaces relax rules; final/sealed tighten them
- JVM does the real check at runtime
- Open-world (compiler) vs closed-world (runtime/final/sealed)
basics
~20 sThe compiler rejects a cast only when the types could never be related, so it can never succeed. If the types are on the same inheritance line, the cast is possible, so the compiler allows it and lets the JVM check at runtime.
solid answer
~50 sThe compiler reasons only about static types. For a reference cast (T) e, it asks: could the object referred to by e plausibly be a T? If T and e's type are connected by inheritance — one is a subtype of the other, or the cast involves an interface that some subclass might implement — then it is plausible, so the compiler permits it and defers the actual verification to a runtime check. If, however, the types are provably unrelated (two unrelated final classes, or two classes with no common subclass), no object could ever satisfy the cast, so the compiler rejects it as an inconvertible-types error. Interfaces relax this: casting between an interface and a non-final class is almost always allowed because a subclass could implement the interface. final classes tighten it: the compiler knows no further subtypes exist, so it can prove some interface casts impossible.
go deeper
Knows some casts are compile errors and others fail at runtime, even if not why.
Can give examples: unrelated classes won't compile; same-hierarchy casts compile but may throw at runtime.
Explains the open-world plausibility check on static types, the interface escape hatch, and how final closes it.
Connects this to sealed types, exhaustiveness, closed-world reasoning, and how API designers use final/sealed to push errors to compile time.
## The core question the compiler asks When the compiler sees a reference cast `(T) expr`, where `expr` has static type `S`, it performs a **cast-conversion legality check**. The question is purely: *is there any possible object for which this cast could succeed at runtime?* It is **not** trying to prove the cast will succeed — only that it is **not impossible**. - If a successful outcome is **conceivable**, the compiler accepts the cast and the JVM does the real runtime check (which may throw `ClassCastException`). - If a successful outcome is **provably impossible** under the type system, the compiler rejects it: *"inconvertible types"* / *"cannot be cast"* — a compile error. ## Cases where the cast is plausible (compiles) 1. **Same inheritance line.** `S` and `T` are related by subtyping (one extends/implements the other). `(Dog) someAnimal` compiles because the `Animal` could be a `Dog`. 2. **Class to interface (non-final class).** `(Comparable) obj` where `obj` is a non-final class type usually compiles, because some subclass of that class *could* implement `Comparable` even if the class itself doesn't. 3. **Interface to interface.** Casting between two interfaces almost always compiles, because a single object could implement both. 4. **Interface to class.** Similar reasoning: some subclass could be both. ## Cases the compiler rejects outright (compile error) 1. **Two unrelated classes.** `(String) someInteger` — `String` and `Integer` share no subtype relationship and neither is an interface, so no object can be both. Inconvertible types. 2. **`final` class breaks the interface escape hatch.** Normally `(SomeInterface) someClassRef` compiles, but if the class is `final` and does **not** implement the interface, the compiler knows there can be **no** subclass to bridge them, so it rejects the cast. Example: `String` is final, so `(SomeRandomInterface) aString` is a compile error if `String` doesn't implement it. 3. **Generics with provably disjoint type arguments**, and primitive vs reference mismatches (different mechanism, but also rejected). ## Why `final` matters so much here The interface escape hatch ("a subclass might implement it") relies on subclasses being able to exist. A `final` class forbids subclasses, so the compiler gains complete knowledge of what that class can and cannot be — and uses it to turn some would-be-runtime failures into compile errors. This is a small but real benefit of marking classes `final`. ## sealed types (Java 17+) extend this reasoning A `sealed` hierarchy enumerates its permitted subclasses, so the compiler again has closed-world knowledge. This powers **exhaustive** `switch` pattern matching and lets the compiler reason more precisely about which casts/patterns are possible — pushing more safety from runtime to compile time. ## The mental model - **Compiler** = open-world plausibility check on **static** types: rejects only the provably impossible. - **JVM** = closed-world certainty check on the **actual** object at runtime: throws when reality contradicts the cast. - `final`/`sealed` shrink the open world, moving some errors earlier (compile time).
- Why does (Comparable) someNonFinalClass usually compile but (Comparable) someFinalClassThatDoesntImplementIt does not?For a non-final class, a subclass could implement Comparable, so the cast is plausible and deferred to runtime. A final class has no subclasses, so the compiler knows no object of that type can be Comparable, making the cast provably impossible — a compile error.
- How do sealed types change the compiler's reasoning about casts and switches?Sealed types give the compiler a closed list of permitted subtypes, enabling exhaustive switch pattern matching and more precise impossibility detection, shifting safety from runtime to compile time.
saying these in an interview costs you the question
- Claiming the compiler validates that a cast will succeed
- Thinking any cast between any two types compiles
- Not knowing why a class-to-interface cast can be a compile error when the class is final
- Conflating compile-time inconvertible-types errors with runtime ClassCastException