Object type casting in Java
Upcasting and downcasting references, what checkcast does at runtime, ClassCastException, which casts the compiler rejects, reference type versus object type, unchecked casts with generics, Class.cast and pattern matching.
On this page
🎯 What is object type casting?
Casting a reference changes the type through which you look at an object, never the object itself. Upcasting (Payment p = cardPayment;) widens the view to a parent type and is always safe, so it's implicit. Downcasting (CardPayment c = (CardPayment) p;) narrows the view back to a subtype, needs an explicit cast, and is checked at runtime: if p actually holds a UpiPayment, you get a ClassCastException.
Casting questions look basic, but they test whether you separate the reference type (what the compiler lets you call) from the object type (what the JVM actually runs). Interviewers ask why one cast fails to compile while another compiles and then throws, what the checkcast instruction does, why (T) obj inside a generic method doesn't throw where you'd expect, and what the modern replacement for cast-after-instanceof looks like.
What you’ll learn
- Why a cast changes the reference type, never the object, and why upcasting is implicit and free
- What the
checkcastinstruction does, including whynullalways passes - Which casts are compile errors (siblings, final classes) and which are checked at runtime
- Why casts to a type variable
Tare unchecked and fail at the caller, and howClass.castgives a real check - How
instanceofpatterns and switch patterns replace check-then-cast in modern Java
For this lesson, we'll focus on upcasting and downcasting of references, ClassCastException, the checkcast bytecode, the compile-time rules that reject impossible casts, reference type versus object type, casting with generics and erasure, Class.cast, and pattern matching as the modern way to cast.
Below the line (out of scope)
- Casting between primitive types (
(int) 3.9, narrowing and widening): covered in Type casting and numeric promotion - The
instanceofoperator in depth: covered in The instanceof operator - Boxing conversions such as
(Integer) objon numbers: see Wrapper classes and autoboxing
The 30-second answer
A reference cast doesn't convert or copy anything; it changes the static type the compiler uses. Upcasting to a supertype is implicit and has no runtime cost. Downcasting needs an explicit cast, and javac emits a checkcast instruction that passes null, passes any object whose class is the target type or a subtype, and otherwise throws ClassCastException. The compiler rejects casts that can never succeed, such as between unrelated classes. Method calls still dispatch on the object's real class, whatever the reference type. Casts to a type variable T are unchecked because of erasure, so failures surface later. In modern Java I replace "instanceof then cast" with if (p instanceof CardPayment card).
Interview radar
Asked very often: Expect reference-type versus object-type questions, downcast failure output, null casts and why upcasting does not change overriding.
Senior rounds probe: I probe checkcast, compiler-rejected casts, unchecked generic casts, heap pollution and when Class.cast gives a real runtime check.
Production angle: This becomes JSON, cache and reflection bugs where data has a wider runtime type than the cast assumes.
01Picture it first: one TV, several remote controls
Think of a smart TV and the remotes that can drive it. A universal remote (an Object reference) only has power and volume. The brand remote (Payment) adds the common menu. The model-specific remote (CardPayment) has every button, including the one for this model's special feature. Switching to a simpler remote is always safe: every TV understands power. Picking up a model-specific remote is a claim that "this TV is that model", and the TV checks before it obeys.
The figure shows three references of different types pointing at one CardPayment object. The object is the same in all three cases; only the list of buttons the compiler lets you press changes.
Three references, Object o, Payment p and CardPayment c, all pointing at the same CardPayment object on the heap, with the methods each reference type allows listed beside it
Notice that p.fee() and c.fee() run the same override, because the method that runs depends on the object. What changes with the reference type is only what compiles: p.last4() doesn't, c.last4() does.
02How casting works under the hood
Upcasting: implicit and free
class Payment { long fee() { return 0; } }
class CardPayment extends Payment {
String last4() { return "4242"; }
@Override long fee() { return 150; }
}
CardPayment card = new CardPayment();
Payment p = card; // upcast: implicit, no bytecode emitted
Object o = card; // also an upcast
System.out.println(p.fee()); // prints: 150 (the object is still a CardPayment)
System.out.println(p == card); // prints: true (same object, same reference value)
// p.last4(); // ❌ cannot find symbol: Payment has no last4()
An upcast can never fail, because every CardPayment is a Payment, so the compiler does it for you and emits nothing. You'll see upcasting constantly without the word "cast": passing a CardPayment to checkout(Payment p), adding it to a List<Payment>, or returning it from a method declared to return Payment.
Downcasting: explicit, and checked at runtime
Try it yourself first
A reconciliation job reads payments from a list and needs card details. Before reading on: which line throws, and what does the exception message say?
List<Payment> payments = List.of(new CardPayment(), new UpiPayment());
for (Payment p : payments) {
CardPayment card = (CardPayment) p; // compiles: CardPayment is a subtype of Payment
System.out.println(card.last4());
}
// prints: 4242
// Exception in thread "main" java.lang.ClassCastException: class UpiPayment cannot be cast
// to class CardPayment (UpiPayment and CardPayment are in unnamed module of loader 'app')
The first iteration works because the object really is a CardPayment. The second compiles fine, since a Payment might be a CardPayment, but the object is a UpiPayment, so the cast fails at runtime. Since Java 11 the message names both classes and their modules or class loaders, which is a lifesaver when the "same" class was loaded twice by two different class loaders (it's a different runtime class then, and the cast fails even though the names match).
What checkcast actually does
javap -c on the loop above shows the cast as a single instruction:
invokeinterface #13 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
checkcast #19 // class Payment ← inserted by javac for List<Payment> (erasure)
astore_2
aload_2
checkcast #21 // class CardPayment ← your explicit cast
astore_3
checkcast takes the reference on top of the stack and decides. The figure shows its whole decision tree.
Decision flow of the checkcast instruction: a null reference passes, an object whose class is the target or a subtype passes unchanged, and anything else throws ClassCastException
Notice three things:
nullalways passes.(CardPayment) nullis fine, and theNullPointerExceptioncomes later when you call a method on it. That's also whyinstanceofand casting disagree onnull:null instanceof CardPaymentisfalse, but the cast succeeds.- The reference comes out unchanged. There's no conversion, copy or new object. If the check passes, the same pointer is now typed
CardPaymentfor the verifier. - You didn't write the first
checkcast.List<Payment>is erased to a rawListwhosenext()returnsObject, so javac inserts a cast at every use site. That's where generics' type safety is actually enforced, and it matters in the Advanced section.
Which casts compile at all
javac rejects a cast it can prove can never succeed (JLS §5.5). The rules, with payment types:
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Real world: the cast that only failed for big orders
- Advanced: generics, erasure, Class.cast and pattern matching
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal