Memra

Designing with inheritance; downcasting & RTTI preview

◈ 5 cards

Substitution vs extension, instanceof-guarded downcasts, ClassCastException, and covariant return types.

Substitution vs extension

Two design intents drive inheritance:

  • Pure substitution ("is-a"). The subclass adds no new public methods; it only overrides. Anywhere the supertype is expected, the subtype slots in cleanly. This is the safest design — calling code never needs to know the concrete type.
  • Extension ("is-like-a"). The subclass adds new methods of its own. Now code that wants those extras must know the concrete type — which means downcasting, and a loss of pure polymorphism.

Prefer pure substitution. Reach for extension (and the downcasting it forces) only when you genuinely need subtype-specific behavior.

Downcasting: narrowing back to the subtype

Downcasting moves down the hierarchy — treating a supertype reference as a specific subtype. Unlike upcasting it is not automatically safe: the object might not actually be that subtype, so it needs an explicit cast and is checked at run time. A wrong cast throws ClassCastException.

Guard every downcast with instanceof. Crucially, instanceof is false for null (a null reference is not an instance of anything), so the check also rules out a NullPointerException:

Worked example: a guarded downcast

void honkIfCar(Vehicle v) {
    if (v instanceof Car) {       // false if v is null or not a Car
        Car c = (Car) v;          // safe: we just checked
        c.honk();                 // Car-specific method
    }
}

Without the instanceof guard, passing a Truck would compile fine but throw ClassCastException at run time. This run-time type query is your first taste of RTTI (Run-Time Type Information) — the JVM tracks every object's real class, which Module 7 develops fully with Class objects and reflection.

Covariant return types

When overriding a method, the subclass may narrow the return type to a subtype of the original. This is a covariant return type and is legal because a Circle is-a Shape — any caller expecting a Shape is satisfied:

class Shape  { Shape copy()  { return new Shape(); } }
class Circle extends Shape {
    @Override Circle copy() { return new Circle(); }   // narrower return: OK
}

Circle.copy() overrides Shape.copy() yet promises the more specific Circle, sparing callers a downcast. You cannot widen a return type this way — only narrow it.

what you writedirectioncheckedcan throwVehicle v = newCar();up (implicit)compile timenoCar c = (Car) v;down (explicit)run timeClassCastExceptioninstanceof, then(Car) vdown, guardedrun timenoinstanceof is false for null, so the guard rules out an NPE too.
Direction decides the risk. Going up the hierarchy is proven by the compiler and can never fail; going down is only a claim until the JVM checks it, which is why the instanceof guard turns row two into row three.
NORMAL ~/memra/learn/comp-308/designing-with-inheritance-downcasting utf-8 LF