Inheritance & Polymorphism

Upcasting, Downcasting & instanceof

15 min Lesson 6 of 14

Upcasting, Downcasting & instanceof

Polymorphism lets you write code that works with a parent type while the actual object at runtime might be any subclass. To do that well, you need to understand two operations: upcasting (treating a subclass object as its parent type) and downcasting (converting it back to the specific subclass). The instanceof operator — and Java 16+'s pattern matching form of it — keeps those conversions safe.

Upcasting — the safe direction

Upcasting means assigning a subclass reference to a parent-type variable. Java does this implicitly because it is always safe: a Dog is always an Animal, so no data can be lost.

class Animal { String name; Animal(String name) { this.name = name; } void speak() { System.out.println(name + " makes a sound"); } } class Dog extends Animal { Dog(String name) { super(name); } @Override void speak() { System.out.println(name + " barks"); } void fetch() { System.out.println(name + " fetches the ball"); } } // Upcasting — no cast syntax needed Animal a = new Dog("Rex"); // implicit upcast a.speak(); // prints: Rex barks (dynamic dispatch still works)
The declared type vs. the runtime type. After Animal a = new Dog("Rex"), the declared (compile-time) type of a is Animal, but the runtime type is still Dog. Method calls respect the runtime type (that is polymorphism from the previous lesson), but the compiler only lets you call methods that exist on Animal. So a.fetch() would be a compile error — even though the object really is a Dog.

Downcasting — the explicit direction

Downcasting means converting the parent-type reference back to a more specific subclass type. You must write the cast explicitly because the compiler cannot guarantee at compile time that the object is actually that subclass. If you get it wrong, Java throws a ClassCastException at runtime.

Animal a = new Dog("Rex"); // upcast // Downcast — explicit syntax required Dog d = (Dog) a; d.fetch(); // now we can call Dog-specific methods: Rex fetches the ball // This would crash at runtime: // Cat c = (Cat) a; // ClassCastException — a is a Dog, not a Cat
Never blindly downcast. Always verify the object type before casting. Casting the wrong type compiles fine but crashes at runtime with a ClassCastException. Use instanceof to guard every downcast.

Checking types with instanceof

The instanceof operator tests whether an object is an instance of a given class (or any of its subclasses). Use it to guard a downcast:

Animal a = new Dog("Rex"); if (a instanceof Dog) { Dog d = (Dog) a; // safe — we know it is a Dog d.fetch(); }

This pattern is safe but a little verbose: you check, then cast, and end up with a second variable. Java 16 introduced a cleaner way.

Pattern matching for instanceof (Java 16+)

With pattern matching, the instanceof check and the cast happen in a single expression, and Java binds the result to a new variable automatically:

Animal a = new Dog("Rex"); // Old way if (a instanceof Dog) { Dog d = (Dog) a; d.fetch(); } // Pattern-matching way (Java 16+) if (a instanceof Dog d) { d.fetch(); // d is already typed as Dog inside this block }

The variable d is only in scope inside the if block (and only where Java can prove the test succeeded). This eliminates an entire category of accidental ClassCastException errors.

A realistic example

Imagine a list of mixed Animal objects. You want to call type-specific behaviour on each one:

import java.util.List; class Cat extends Animal { Cat(String name) { super(name); } @Override void speak() { System.out.println(name + " meows"); } void purr() { System.out.println(name + " purrs"); } } public class Main { public static void main(String[] args) { List<Animal> animals = List.of( new Dog("Rex"), new Cat("Whiskers"), new Dog("Buddy") ); for (Animal a : animals) { a.speak(); // polymorphic — always works if (a instanceof Dog d) { d.fetch(); // Dog-specific } else if (a instanceof Cat c) { c.purr(); // Cat-specific } } } }
Prefer polymorphism over instanceof chains. If you find yourself writing long if/else instanceof chains, that is often a sign that the behaviour belongs inside an overridden method. instanceof is the right tool when you genuinely need type-specific behaviour that cannot be put into the parent class — for example, when working with third-party classes you cannot change, or when mixing unrelated hierarchies.

instanceof with inheritance depth

instanceof returns true for the object's own class and any ancestor in the hierarchy:

Dog rex = new Dog("Rex"); System.out.println(rex instanceof Dog); // true System.out.println(rex instanceof Animal); // true — Dog extends Animal System.out.println(rex instanceof Object); // true — everything extends Object

Summary

  • Upcasting is implicit and always safe — a subclass reference can be stored in a parent-type variable.
  • Downcasting requires an explicit cast and can fail at runtime if the object is the wrong type.
  • Use instanceof to guard every downcast and avoid ClassCastException.
  • Java 16+ pattern matching for instanceof combines the test and the cast in one step, binding a typed variable automatically.
  • instanceof also returns true for ancestor types, not just the exact class.