Course contents

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.

PremiumOften askedLesson13 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: one TV, several remote controls
  2. How casting works under the hood
  3. Real world: the cast that only failed for big orders
  4. Advanced: generics, erasure, Class.cast and pattern matching
  5. How to say it in the interview

🎯 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 checkcast instruction does, including why null always passes
  • Which casts are compile errors (siblings, final classes) and which are checked at runtime
  • Why casts to a type variable T are unchecked and fail at the caller, and how Class.cast gives a real check
  • How instanceof patterns 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)

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.

Fig. 1

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

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.

Fig. 2

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

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:

  • null always passes. (CardPayment) null is fine, and the NullPointerException comes later when you call a method on it. That's also why instanceof and casting disagree on null: null instanceof CardPayment is false, 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 CardPayment for the verifier.
  • You didn't write the first checkcast. List<Payment> is erased to a raw List whose next() returns Object, 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

Practise this topicInheritance & polymorphism: interview questions & scenarios
Further reading (7)