Exercises — Method overriding — when and why
Parent: Method overriding — when and why. This page is a self-test ladder. Cover the solution, try it, then reveal. Every symbol used here is built in the parent note — if a term feels new, re-read Inheritance, Polymorphism, and super keyword first.
One notation note before we start. On this page, text wrapped in ==double equals== marks the single most important phrase of a sentence — treat it as a highlighter, the words you would underline if this were paper. Nothing mathematical; just emphasis you should be able to recite.
Before the ladder, pin down two words used everywhere below, and a picture of the whole mechanism.
The diagram below (Figure 1) shows a Shape-typed variable holding a real Circle, and how the call s.area() walks through the object to the Circle vtable.

Before Level 1, here is the whole recognition rule as a picture (Figure 2): three columns — name, params, access/kind — and which one each mechanism keys on.

Level 1 — Recognition
Goal: can you spot overriding versus its look-alikes just by reading?
Exercise 1.1 (L1)
Look at each pair. Say whether it is overriding, overloading, or hiding (static/field shadowing).
class A {
void greet(String name) { ... } // (a)
void greet() { ... } // (b) vs (a) in same class A
static void tag() { ... } // (c)
}
class B extends A {
void greet(String name) { ... } // (d) vs (a)
static void tag() { ... } // (e) vs (c)
}For each of (a) vs (b), (d) vs (a), (e) vs (c), name the mechanism. (Use Figure 2 as your checklist: name, params, kind.)
Recall Solution 1.1
- (a) vs (b): overloading — same method name
greet, but different parameters (Stringvs none), both in the same class. The differing column in Figure 2 is params. Why compile time? the compiler already knows the argument types you wrote at the call site, so it can pick the matching version before the program even runs. - (d) vs (a): overriding — child
Bredefines the exact signaturegreet(String)from parentA. Name and params match, and it's an instance method. Why runtime? the actual object may not be known until execution (it could arrive from a list, a factory, user input), so the choice must wait for the real object's vtable. - (e) vs (c): hiding — both are
static. The differing column in Figure 2 is kind. Statics belong to the class, not any object, so there is no object to carry a vtable pointer; the compiler resolves them by the declared type. That is hiding, not overriding.
Exercise 1.2 (L1)
True or false: In Shape s = new Circle(); s.area();, the compiler decides Circle.area will run.
Recall Solution 1.2
False. The compiler only checks that area() is legal on the declared type Shape (it compiles). The runtime looks at the object stored in s — a Circle — reads its vtable, and dispatches to Circle.area. Why split it this way? the compiler guarantees safety (the call is even allowed) using the type it can see; the runtime guarantees correctness (the real object's version) using the vtable pointer. Neither could do the other's job alone.
Level 2 — Application
Goal: predict what the code prints, given dispatch rules.
Exercise 2.1 (L2)
class Shape:
def area(self): raise NotImplementedError
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
shapes = [Circle(2), Rectangle(3, 4), Circle(1)]
print(round(sum(s.area() for s in shapes), 2))What number prints?
Recall Solution 2.1
Each s.area() dispatches on the real object:
Circle(2)→Rectangle(3,4)→Circle(1)→
Exact sum , so it prints 27.7.
Floating-point caveat: 3.14 * 2**2 in binary floating point is not exactly 12.56 — it is a hair off (like 12.559999999999999). That is why the code wraps the sum in round(..., 2): round snaps to 2 decimal places, giving 27.7. Note also Python's round uses banker's rounding (ties go to the nearest even digit), which does not affect us here but is worth knowing. Lesson: when comparing float results, always round or use a tolerance — never test raw floats for equality.
Exercise 2.2 (L2)
class Account:
def describe(self): return "Account"
class Savings(Account):
def describe(self):
return super().describe() + " > Savings"
class Premium(Savings):
def describe(self):
return super().describe() + " > Premium"
print(Premium().describe())What string prints?
Recall Solution 2.2
super() climbs one link at a time. Premium.describe calls Savings.describe, which calls Account.describe:
Accountreturns"Account"Savingsappends →"Account > Savings"Premiumappends →"Account > Savings > Premium"
Prints Account > Savings > Premium. This is "extend", not "replace" — see super keyword.
Level 3 — Analysis
Goal: reason about the vtable and about substitutability.
Exercise 3.1 (L3)
Shape defines area and perimeter. Circle(Shape) overrides only area. Sketch the entries of vtable(Circle). Which body runs for Circle().perimeter()?
Recall Solution 3.1
Recall from the top of the page: a vtable maps each method name to the code that runs. A child's vtable is a copy of the parent's, then overrides overwrite matching entries. In plain text:
| method | vtable(Circle) points to |
|---|---|
area |
Circle.area (overwritten) |
perimeter |
Shape.perimeter (copied, untouched) |
Circle overwrote area, but left perimeter untouched, so it still points at Shape.perimeter. Therefore Circle().perimeter() runs Shape.perimeter — inherited, not overridden. In the figure below (Figure 3), only the area cell got repainted.

Exercise 3.2 (L3)
A parent method is declared public. A student overrides it as private in the child, arguing "it's my class, I'll lock it down." Why does the compiler reject this, and which principle is violated?
Recall Solution 3.2
It violates the Liskov Substitution Principle. Restated in one line: anywhere the parent type is expected, an object of the child type must work as a drop-in replacement without surprising the caller. Here, any code holding a Parent reference may legally call the public method. If the real object is the child and its version is private, that legal call would suddenly be forbidden — the child is not substitutable for the parent. Why the compiler can catch this: access level is fixed in the source text, so the compiler checks it statically and refuses to compile. The rule: you may widen access (e.g. protected → public) but never narrow it.
Level 4 — Synthesis
Goal: convert an if/else type-switch into an override design, and see DRY pay off.
Exercise 4.1 (L4)
Refactor this into a class-per-type override design that obeys the Open-Closed Principle:
def cost(vehicle_type, km):
if vehicle_type == "car": return 10 + 2*km
elif vehicle_type == "bike": return 5 + 1*km
elif vehicle_type == "truck": return 20 + 5*kmThen show why adding a "van" type is cleaner in your design, and point to exactly where DRY (Don't Repeat Yourself) is honored.
Recall Solution 4.1
Give each type its own class overriding one cost method, and call .cost(...) directly on the object — no free function needed:
class Vehicle:
def cost(self, km): raise NotImplementedError
class Car(Vehicle):
def cost(self, km): return 10 + 2*km
class Bike(Vehicle):
def cost(self, km): return 5 + 1*km
class Truck(Vehicle):
def cost(self, km): return 20 + 5*km
# Call the method straight on whatever object you hold:
Truck().cost(4) # -> 40 (dispatch picks Truck.cost)
Bike().cost(10) # -> 15 (dispatch picks Bike.cost)Notice we did not rename anything into a new total(...) wrapper — the whole point is that vehicle.cost(km) at the call site already does the branching for you, because dispatch reads the real object's vtable.
Where DRY lives: in the original code, the if/else branching logic is written once but must be re-copied into every function that ever needs to switch on vehicle type. In the override design, the "which formula?" decision lives in exactly one place — the dispatch mechanism — and each formula lives in exactly one class. No branch is ever duplicated. That is DRY.
Adding van: just write a new Van(Vehicle) class. You never touch any existing class or call site — that is Open/Closed: open to extension (new subclass), closed to modification (no edits to tested code). The if/else version instead forced you to edit the one growing function every time. This is Polymorphism doing the branching for you.
Exercise 4.2 (L4)
Using your L4.1 design, compute Truck().cost(4) + Bike().cost(10).
Recall Solution 4.2
Truck().cost(4)→Bike().cost(10)→
Sum .
Level 5 — Mastery
Goal: reason at the edges — covariant returns, abstract contracts, final, interfaces, degenerate objects.
Exercise 5.1 (L5)
Parent Shape.copy() returns a Shape. Circle overrides copy() to return a Circle. Is this a legal override? Name the feature.
Recall Solution 5.1
Legal. This is a covariant return type: an override may return a subtype of the parent's declared return. Circle is-a Shape, so any caller expecting a Shape back still gets a valid Shape. Substitutability holds → Liskov Substitution Principle satisfied. (You may narrow the return type; you may not narrow access — those two "narrowings" have opposite rules, because a narrower return still satisfies callers, but narrower access forbids a call they were promised.)
Exercise 5.2 (L5)
Shape is an abstract class whose area() is abstract (no body). Circle provides a body; Triangle forgets to. What happens, and why is this a feature?
Recall Solution 5.2
Triangle remains abstract and cannot be instantiated — the compiler (Java) or interpreter (Python abstractmethod) refuses Triangle(). This is a feature: an abstract method is a contract that says "every concrete child must fulfill this promise." Overriding here is mandatory, not optional. Forgetting it is caught early instead of blowing up as NotImplementedError at runtime deep in production.
Exercise 5.3 (L5)
Predict the output and explain the dispatch precisely:
class A:
def who(self): return "A"
def call(self): return self.who() # note: self.who(), not A.who()
class B(A):
def who(self): return "B"
print(A().call(), B().call())Recall Solution 5.3
Prints A B.
call is defined only in A, but it invokes self.who(). The dispatch of who() uses the real object bound to self:
A().call():selfis anA→who→"A".B().call():selfis aB, andBoverrodewho, so even thoughcalllives inA,self.who()dispatches toB.who→"B".
Why: self carries the object's vtable pointer. self.who() re-looks-up who in that vtable (B's), regardless of which class the calling code physically lives in. This is the deep point: inherited methods still dispatch through the real object, so A.call didn't need editing to get B's behavior — that's overriding + Polymorphism working together.
Exercise 5.4 (L5)
In Java, Shape.area() is declared final. A student writes class Circle extends Shape with its own area() body, expecting it to override the parent's. What does the compiler do, and why would a designer mark a method final in the first place?
Recall Solution 5.4
The code does not compile — Java rejects any attempt to override a final method, reporting something like "area() in Circle cannot override area() in Shape; overridden method is final." A final method is explicitly sealed against overriding: its entry in every subclass vtable is frozen to the parent's code, so no child can ever repaint that cell.
Why a designer does this: to guarantee an invariant that no subclass is allowed to break. Classic reasons:
- a security or validation check that must run exactly as written (a child must not weaken it);
- a method other code contractually relies on (e.g. a
hashCode/equalspairing, or a template-method skeleton whose steps are fixed).
It is the deliberate opposite of an Open-Closed Principle extension point — the designer is saying "this behavior is closed to extension on purpose." So the full override rule, at mastery level, is: a method is overridable only if it is an instance method and not final and visible to the child. final removes the middle condition.
Exercise 5.5 (L5)
An interface Greeter provides a default method hello(). Two interfaces, Greeter and Farewell, both provide a default hello(), and Robot implements Greeter, Farewell. What must Robot do?
Recall Solution 5.5
This is the diamond / multiple-inheritance conflict: two inherited default bodies with the same signature and no single "most-derived" winner, so the vtable cannot pick automatically. Java requires Robot to ==explicitly override hello()== to resolve the ambiguity; inside it, Robot may call a specific one with Greeter.super.hello(). Why forced: dispatch relies on a unique entry per signature; when inheritance offers two equally-valid entries, the language refuses to guess and makes the programmer decide. (Python's super() instead uses a deterministic MRO — method resolution order — to linearize the parents, so it picks one without erroring; the concept both languages must solve is "which of several inherited bodies wins?") See Abstract classes and interfaces.
Recall One-line self-check before you leave
Same signature? ::: required for overriding (else overloading/hiding); signature = name + parameter types, return type excluded
Who picks the body? ::: the runtime object, via its vtable
Can you narrow access on override? ::: no — widen only; but you may narrow the return type (covariant)
Can a final method be overridden? ::: no — final seals the vtable entry against any child
Two interface defaults with the same signature? ::: the class must explicitly override to resolve the ambiguity
Adding a new type edits old code? ::: no, if you used overriding — that's Open/Closed and DRY