Memra

Designing with inheritance; downcasting & RTTI preview

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.

NORMAL ~/memra/learn/comp-308/designing-with-inheritance-downcasting utf-8 LF