🧬 Inheritance & Polymorphism · Intermediate

Upcasting & downcasting in Java

Implicit upcasts, explicit downcasts and ClassCastException.

🧩 The mysteryThe compiler happily accepts (Dog) animal. Then, at runtime: ClassCastException. Why did the compiler let you walk into that?

Upcasting: always safe

Treating a Dog as an Animal is an upcast. Every Dog is an Animal, so it's implicit — no cast needed, nothing can go wrong. You just see fewer methods.

Dog d = new Dog();
Animal a = d;   // implicit upcast

Downcasting: a promise you must keep

Going back — Animal to Dog — is a downcast. It needs an explicit cast (Dog) a, and the JVM checks it at runtime. If the object really is a Dog, you unlock Dog's own methods.

Animal a = new Dog();
Dog d = (Dog) a;  // checked at runtime
d.fetch();        // Dog methods unlocked
🔮 Predict it

A real Dog

What does this print?

class Animal { }
class Dog extends Animal {
    String fetch() { return "stick"; }
}
void main() {
    Animal a = new Dog();
    Dog d = (Dog) a;
    System.out.println(d.fetch());
}
  1. stick
  2. Compile error
  3. Throws ClassCastException
Show the answer

stick. The object really is a Dog, so the runtime check passes and fetch() becomes available.

🔮 Predict it

A Cat in disguise

Now the Animal is a Cat. What happens?

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
void main() {
    Animal a = new Cat();
    Dog d = (Dog) a;
    System.out.println("ok");
}
  1. ok
  2. Compile error
  3. Throws ClassCastException
Show the answer

It throws **ClassCastException**. The compiler allowed the cast because an Animal *might* be a Dog. At runtime the JVM sees a Cat and refuses.

⚠️ The trap

Unrelated classes

Dog and Car share no superclass chain, so no object could ever be both. The compiler knows the cast can't succeed and rejects it outright — compile error, not an exception.

class Dog { }
class Car { }
Dog d = new Dog();
Car c = (Car) d;  // compile error
🤔 Think first

What does a cast do to the object?

After Dog d = (Dog) animal;, has the object been converted into a Dog?

Think about it, then reveal the answer

Nothing happens to the object. Objects never change class. A reference cast only changes the static type the compiler uses for that reference — after the JVM verifies the object already is one.

💼 In the real world

In real projects

Old pre-generics code was full of casts like (String) list.get(0), and a wrong one crashed at runtime. Today you'll mostly cast inside equals(Object) — and modern code prefers pattern matching (next lesson) to check and cast in one safe step.

Key takeaways

  1. Upcast: implicit, always safe
  2. Downcast: explicit (Dog) a, checked at runtime
  3. Wrong downcast throws ClassCastException
  4. Casts between unrelated classes don't compile
🤯 Did you know?

Generics are erased at runtime, so the compiler secretly inserts casts for you: String s = list.get(0); compiles to a checkcast instruction in the bytecode.

Practice questions

What does this print?

class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
void main() {
    Animal a = new Cat();
    Dog d = (Dog) a;
    System.out.println("ok");
}
  1. ok
  2. Compile error
  3. Throws ClassCastException
Check your answer

Throws ClassCastException. The compiler accepts the cast because an Animal might be a Dog. At runtime the object is a Cat, so the cast fails.

What does this print?

class Animal { }
class Dog extends Animal {
    String fetch() { return "ball"; }
}
void main() {
    Animal a = new Dog();
    Dog d = (Dog) a;
    System.out.println(d.fetch());
}
  1. ball
  2. Compile error
  3. Throws ClassCastException
Check your answer

ball. The object really is a Dog, so the downcast succeeds and unlocks Dog's own methods.

Next: check the type and cast in one move — instanceof pattern matching.