Skip to content
Part IA Michaelmas Term

Casting: Widening and Narrowing

Class Casting: Upcasting vs Downcasting

Widening (Upcasting)

Widening or upcasting means treating an object through a reference of a superclass type. It is always safe — because the object genuinely is-a instance of the superclass — and it is often implicit (the compiler inserts it for you):

class Animal { }
class Dog extends Animal { }

Dog dog = new Dog();
Animal a = dog;         // widening — implicit, always safe
Object o = dog;         // implicit cast to Object, also widening

Even though the reference a has compile-time type Animal, the object on the heap is still a Dog. The runtime type never changes — only the type of the reference through which you access it changes.

Widening is the mechanism that enables polymorphism: if a method expects an Animal, you can pass any subtype:

void feed(Animal a) { a.eat(); }

feed(new Dog());
feed(new Cat());
feed(new Bird());

Narrowing (Downcasting)

Narrowing or downcasting means treating an object through a reference of a subclass type — going from a more general type to a more specific one. This is explicit and can fail at runtime:

Animal a = new Dog();
Dog d = (Dog) a;     // narrowing — explicit cast, succeeds because a is actually a Dog
d.bark();            // now we can call Dog-specific methods

The cast tells the compiler “trust me, I know this is really a Dog”. The JVM checks at runtime that the object is indeed an instance of that type. If it is not, ClassCastException is thrown:

Animal a = new Cat();
Dog d = (Dog) a;     // ClassCastException at runtime — a Cat cannot be cast to Dog

The instanceof Guard

Before downcasting, check the type with instanceof. Java 16+ supports pattern-matching instanceof, which combines the test and declaration:

if (a instanceof Dog d) {
    d.bark();  // d is in scope and already typed as Dog
}

The pre-Java-16 form (still valid and examinable):

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

The pattern-matching form avoids the redundant type name and the separate cast, reducing the chance of error.

Why the Compiler Won’t Let You Call Subclass Methods

Even if you know an Animal reference points at a Dog object, the compiler enforces the declared type:

Animal a = new Dog();
a.bark();  // compile error: bark() is not defined on Animal

The compiler only knows about methods on Animal. You must downcast to Dog to call bark. This is a deliberate safety feature — it forces you to be explicit about assumptions regarding runtime type.

Common Tripos Scenario

A classic Tripos tracing question involves code like:

class A {
    void m() { System.out.println("A.m"); }
}
class B extends A {
    void m() { System.out.println("B.m"); }
    void n() { System.out.println("B.n"); }
}
class C extends B {
    void m() { System.out.println("C.m"); }
}

public static void main(String[] args) {
    A x = new C();
    B y = (B) x;
    A z = new B();

    x.m();            // "C.m" — dynamic dispatch, x's runtime type is C
    ((C) x).n();      // "B.n" — cast succeeds, n() inherited from B
    z.m();            // "B.m" — dynamic dispatch, z's runtime type is B
    ((B) z).n();      // "B.n" — cast succeeds, z is a B
    B w = (B) new A();  // ClassCastException at runtime
}

The key skill is tracking the declared type (what the compiler allows) and the runtime type (what the JVM dispatches to) for each variable at each point in the execution.

The getClass() Method

Every object inherits getClass() from Object. It returns the object’s runtime class:

Animal a = new Dog();
System.out.println(a.getClass().getName());  // prints "Dog", not "Animal"

getClass() returns the runtime type, regardless of the reference type. It is frequently used in .equals() implementations and for debugging.

Casting with Interfaces

The same rules apply to interfaces:

List<String> list = new ArrayList<>();
ArrayList<String> al = (ArrayList<String>) list;  // downcast to concrete type

An interface reference can be cast to any class that implements it, or to any sub-interface. The runtime check ensures the object actually implements the target type.