Inheritance and polymorphism
Inheritance is the feature people learn first and reach for too often. It is genuinely useful, it is the mechanism behind polymorphism, and it is also the tightest coupling Java offers — a subclass depends on its parent's internals, not just its published methods.
So this lesson does two things: teach it properly, and be honest about when not to use it.
The mechanism
class Plan {
private final int tiffins;
Plan(int tiffins) {
this.tiffins = tiffins;
}
int tiffins() {
return tiffins;
}
String name() {
return "Plan";
}
int pricePaise() {
return tiffins * 8_235;
}
}
class TrialPlan extends Plan {
TrialPlan(int tiffins) {
super(tiffins);
}
@Override
String name() {
return "Trial";
}
@Override
int pricePaise() {
return 0;
}
}
class StudentPlan extends Plan {
StudentPlan(int tiffins) {
super(tiffins);
}
@Override
String name() {
return "Student";
}
@Override
int pricePaise() {
return super.pricePaise() * 90 / 100;
}
}
extends says "everything Plan has, plus these changes". super(...) calls
the parent's constructor; super.pricePaise() calls the parent's version of a
method you are overriding.
A subclass constructor must call a superclass constructor first. If you do
not write super(...), Java inserts super() — and if the parent has no
no-argument constructor, that is a compile error telling you to supply one
explicitly.
Java has single inheritance: one extends, no more. That restriction is
deliberate, and interfaces are the answer to what you lose.
Polymorphism, which is the actual point
Plan[] plans = {new MonthlyPlan(26), new TrialPlan(3), new StudentPlan(26)};
for (Plan p : plans) {
System.out.printf("%-14s Rs %d.%02d%n", p.name(), p.pricePaise() / 100, p.pricePaise() % 100);
}
Monthly Rs 2141.10
Trial Rs 0.00
Student Rs 1926.99
The loop variable is declared Plan. The method that runs is the one belonging
to the object's actual type, decided at runtime. This is dynamic dispatch,
and it is what lets you add a fourth plan without touching the loop.
That is the reason inheritance exists. Not code reuse — reuse is a side
effect, and usually a worse way of getting it than composition. The value is
being able to write code against Plan that works for plans that do not exist
yet.
@Override is not optional
It is technically optional. Treat it as mandatory.
class SilentlyWrong extends Plan {
// meant to override pricePaise()
int pricePaise(int discountPercent) {
return 0;
}
}
price via Plan reference: 214110
The discount never applied. The parameter makes this a different method — an
overload, not an override — so Plan.pricePaise() still runs. It compiles, it
runs, and it is wrong.
Add the annotation and the compiler catches it:
error: method does not override or implement a method from a supertype
@Override
^
@Override turns a silent behaviour bug into a build failure. Put it on
every method you intend to override, including toString, equals and
hashCode.
Overriding rules worth knowing:
| Rule | Detail |
|---|---|
| Signature | Name and parameter types must match exactly |
| Return type | Same, or a subtype (covariant returns) |
| Access | May widen (protected to public), never narrow |
| Exceptions | May throw fewer or narrower checked exceptions, never broader |
final methods |
Cannot be overridden at all |
static methods |
Are hidden, not overridden — dispatch is by reference type |
private methods |
Are not visible to subclasses, so never overridden |
The constructor bug
This is the one that produces a null nobody can explain.
class Broken {
Broken(int tiffins) {
report(); // overridable, called from a constructor
}
void report() {
System.out.println("Broken.report");
}
}
class BrokenChild extends Broken {
private final int dailyLimit;
private final List<String> notes = new ArrayList<>();
BrokenChild(int tiffins) {
super(tiffins);
this.dailyLimit = tiffins;
System.out.println("after super(), dailyLimit = " + dailyLimit + ", notes = " + notes);
}
@Override
void report() {
System.out.println("BrokenChild.report sees dailyLimit = " + dailyLimit + ", notes = " + notes);
}
}
BrokenChild.report sees dailyLimit = 0, notes = null
after super(), dailyLimit = 26, notes = []
notes has a field initialiser and is still null. Here is why.
Construction runs parent-first: BrokenChild's constructor calls super(...)
before anything else, so Broken's constructor runs to completion before any
of BrokenChild's fields are initialised — including the ones with
initialisers written on the declaration. And report() dispatches
polymorphically, so the parent's constructor calls the child's override, which
reads fields that do not exist yet.
Never call an overridable method from a constructor. Make it private,
final, or static, or do the work after construction. This is also why the
encapsulation lesson marked setTiffins as final when the constructor called
it.
Fields are not overridden — they are hidden
class Parent { String label = "parent"; }
class Child extends Parent { String label = "child"; }
Parent asParent = new Child();
Child asChild = new Child();
System.out.println(asParent.label);
System.out.println(asChild.label);
System.out.println(asParent.describe());
parent
child
Child.describe
The same object reports two different labels depending on the reference type,
while the method call dispatches to Child. Methods dispatch on the object;
fields resolve on the reference type.
This is a trap with no upside. Do not reuse a field name in a subclass — keep
fields private and the question cannot arise.
final, and closing a class
final class— cannot be extended.Stringis one.finalmethod — cannot be overridden.finalfield — assigned once.
Making a class final is not unfriendly. A class that was never designed to be
extended and is extended anyway will break when you change its internals, and
then the breakage is your problem. Design for inheritance and document it, or
forbid it.
When not to use inheritance
Two tests.
Is it genuinely an "is-a"? A StudentPlan is a Plan. A Subscriber is not
a Route, however convenient sharing that code would be.
Would composition do? Usually it would:
// inheritance: SmsNotifier IS A HttpClient, which is untrue
class SmsNotifier extends HttpClient { ... }
// composition: SmsNotifier HAS A HttpClient, which is true
class SmsNotifier {
private final HttpClient http;
SmsNotifier(HttpClient http) {
this.http = http;
}
}
The composed version exposes only what SmsNotifier chooses, can swap the client
for a fake in a test, and does not inherit forty public methods that have nothing
to do with sending an SMS.
The costs of inheritance, stated plainly:
- You get the parent's entire public surface whether it suits you or not.
- A change to the parent can break the child without either file being edited.
- Your one
extendsis spent, forever. - A deep hierarchy means reading four files to understand one method call.
Prefer composition. Use inheritance when several types genuinely share an identity and you want to treat them uniformly — which, by the end of this module, you will more often do with an interface or a sealed type.
Check your work
What does super(...) do, and where must it be? Calls a superclass
constructor, and it must be the first statement. Java inserts super()
automatically when you omit it, which fails to compile if the parent has no
no-argument constructor.
Why does the loop over Plan[] print three different prices? Dynamic
dispatch: the method belonging to the object's actual runtime type runs, not the
one belonging to the reference type.
Why is @Override important when it is optional? Without it, a mistyped name
or a wrong parameter list silently creates a new overloaded method and the
original still runs. With it, the compiler reports method does not override or
implement a method from a supertype.
Why was notes null inside report() despite having an initialiser? The
superclass constructor runs before any subclass field initialiser. Calling an
overridable method from a constructor therefore reaches a half-built object.
How do you avoid that? Do not call overridable methods from constructors —
make them private, final or static, or move the work out of construction.
What is the difference between overriding a method and hiding a field? Methods dispatch on the object's actual type; fields resolve on the reference type, so the same object can report two different values.
When should you prefer composition? Whenever the relationship is not a
genuine "is-a", and whenever you only want a few of the other type's
capabilities. Composition exposes what you choose and leaves extends free.
Practice 3, the discount that never applied. pricePaise(int) is an overload,
not an override, so Plan.pricePaise() runs and the price is the undiscounted
214110. Adding @Override turns it into a compile error. The fix is to drop
the parameter and take the discount from a field set in the constructor:
class StudentPlan extends Plan {
@Override
int pricePaise() {
return super.pricePaise() * 90 / 100;
}
}
Practice 5, Notifier by composition.
class SmsNotifier {
private final MessageGateway gateway;
SmsNotifier(MessageGateway gateway) {
this.gateway = gateway;
}
void remind(Subscriber s) {
gateway.send(s.phone(), "Your tiffin is on the way");
}
}
The test passes a fake MessageGateway that records calls instead of sending
anything. With extends there is nothing to substitute — which is the practical
reason composition wins far more often than the "is-a" argument does.
Practice
-
Build the plan hierarchy.
Planwithname()andpricePaise(), thenMonthlyPlan,TrialPlan(free) andStudentPlan(10% off). Put them in an array typedPlan[]and print a price list. Then add a fourth plan without touching the printing loop. -
Call
super. MakeStudentPlan.pricePaise()usesuper.pricePaise()rather than recalculating. Then change the base price inPlanand confirm the student price follows. -
Break an override on purpose. Add a parameter to an overriding method and run it — the base version still runs, silently. Then add
@Overrideand read the compile error. -
Reproduce the constructor bug. A parent whose constructor calls an overridable method, a child with a field initialiser, and a print inside the override. Confirm you see
0andnull. Then fix it by making the methodfinaland observe that the child can no longer override it at all — which is the trade. -
Replace inheritance with composition. Write
SmsNotifier extends HttpClient, then rewrite it to hold one. Then write a fake gateway that records what it was asked to send, and use it to testremind()without any network. Note which version you could test. -
Harder — break the substitution. Write
RectanglewithsetWidth/setHeightandarea(), thenSquare extends Rectanglethat keeps both sides equal. Now write a method taking aRectangle, setting width to 5 and height to 4, and asserting the area is 20. Pass it aSquareand watch it fail. Nothing you can do insideSquarefixes it. This is the Liskov substitution principle arriving as a concrete failure, and the lesson is that "a square is a rectangle" being true in mathematics does not make it true for mutable objects.
Next: interfaces — how to share behaviour without spending your one extends.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship