CMP-220 · Lesson 5 of 12 · 20 min

Inheritance

Northway Sensing has sold three generations of counter. About 600 older NS-3 nodes still work in the field; they count with inductive loops cut into the pavement and estimate vehicle length from how long a car sits over a pair of loops. The 4,800 NS-4 nodes use radar and measure speed and length directly. This spring the company starts shipping the NS-5, which adds a camera and classifies vehicles on the node itself, sending only counts and never images. Tally.java handled the three models with flags and if statements scattered through every method: if (model == 3) ... else if (model == 4) .... Dana asks Jonah to model the node types properly. The first tool he reaches for is inheritance, and this lesson shows both its power and its traps.

The is-a relationship

Inheritance lets one class, the subclass or derived class, be defined as an extension of another, the superclass or base class. The subclass automatically has all the superclass's fields and methods, can add new ones, and can replace, or override, inherited methods with its own versions. Inheritance expresses an is-a relationship: an NS-4 radar node is a counter node, so RadarNode extends CounterNode. A useful check is to say the sentence aloud. If 'every RadarNode is a CounterNode, and can be used anywhere a CounterNode is expected' is true without exceptions, inheritance may fit. If the honest sentence is 'a Station has nodes', the relationship is composition, from lesson 4.

A UML class diagram. At the top, a box for CounterNode lists the fields id, lanes and lastUpload and the methods recordUpload, isSilent and describe. Three boxes below, LoopNode, RadarNode and CameraNode, each connect upward to CounterNode with a line ending in a hollow triangle at CounterNode. LoopNode adds loopSpacingCm and overrides estimateLength. RadarNode adds firmware and hasSpeed. CameraNode adds modelVersion and classifiesOnNode.
The node hierarchy in UML. A hollow triangle on the superclass marks generalization: each subclass is a CounterNode.

In UML, inheritance is drawn as a line from the subclass to the superclass with a hollow triangle at the superclass end, called generalization. The superclass holds what all nodes share: an identifier, the watched lanes, the time of the last upload, and methods such as recordUpload and isSilent. Each subclass adds only what makes it different. Code that works with CounterNode, such as the silence monitor that alerts operators, now works with all three models without knowing which is which.

Inheritance in Java

In Java a class names its superclass with the keyword extends. A class can extend only one other class, a rule called single inheritance of implementation; lesson 7 shows how interfaces provide the flexibility that multiple inheritance offers in other languages. Every class without an explicit superclass extends java.lang.Object, which supplies toString, equals, hashCode and a few other methods, so every Java class is part of one tree rooted at Object. A subclass constructor must begin by calling a superclass constructor with super(...); if it does not, the compiler inserts a call to the superclass's no-argument constructor, which fails to compile if there is none. This chaining guarantees that the superclass part of an object is initialized, and its invariants checked, before the subclass part.

LoopNode extends CounterNode: it calls super(...) first, overrides estimateLength and extends describe by calling super.describe().
public class CounterNode {
    private final int id;
    private final Set<Integer> lanes;
    private Instant lastUpload;

    public CounterNode(int id, Set<Integer> lanes) {
        if (id < 1 || id > 65535) throw new IllegalArgumentException("id " + id);
        this.id = id;
        this.lanes = Set.copyOf(lanes);
    }
    public int id() { return id; }
    public boolean isSilent(Instant now, Duration limit) {
        return lastUpload == null || Duration.between(lastUpload, now).compareTo(limit) > 0;
    }
    public void recordUpload(Instant t) { lastUpload = t; }
    public String describe() { return "node " + id + " watching lanes " + lanes; }
    public int estimateLength(Detection d) { return d.lengthCm(); }   // default: measured
}

public class LoopNode extends CounterNode {
    private final int loopSpacingCm;          // distance between the two loops

    public LoopNode(int id, Set<Integer> lanes, int loopSpacingCm) {
        super(id, lanes);                     // must come first: initialize the CounterNode part
        this.loopSpacingCm = loopSpacingCm;
    }
    @Override
    public int estimateLength(Detection d) {  // loops cannot measure length directly
        double speedMs = loopSpacingCm / 100.0 / d.gapSeconds();
        return (int) Math.round(speedMs * d.occupancySeconds() * 100) - 180;  // minus effective loop length
    }
    @Override
    public String describe() { return "NS-3 loop " + super.describe(); }
}

Two details in the listing deserve attention. The @Override annotation tells the compiler that a method is meant to override an inherited one; if the name or parameter list is misspelt, so that it accidentally declares a new method instead, compilation fails. Always use it. And super.describe() calls the superclass's version of the method from inside the override, which lets the subclass extend inherited behavior rather than copy and replace it. The private fields of CounterNode are inherited, in the sense that every LoopNode object contains them, but LoopNode's code cannot touch them directly; it must use CounterNode's public or protected methods. That keeps the superclass in charge of its own invariants.

protected, final and the design for inheritance

A protected member is visible to subclasses (and to classes in the same package). It is tempting to make superclass fields protected so subclasses can use them freely, but that exposes them to every subclass anyone will ever write, which is almost as open as public. Prefer private fields with protected or public methods, exactly as in lesson 2. The keyword final works in the other direction: a final method cannot be overridden, and a final class, such as Detection or String, cannot be extended at all. Joshua Bloch's well-known advice in Effective Java is to design and document for inheritance or else prohibit it, because a class that was never meant to be extended can be broken by subclasses in subtle ways.

The Liskov substitution principle

In 1987 Barbara Liskov stated the condition that makes inheritance safe: objects of a subclass must be usable anywhere objects of the superclass are expected, without the program noticing any difference in correctness. A subclass may do more, but it must not demand more of its callers or promise less. In practice this means an override must accept every input the original accepted, must not throw new kinds of exceptions callers are not prepared for, must keep every invariant of the superclass and must meet every promise the superclass's documentation makes. The Liskov substitution principle, LSP for short, is the L in the SOLID principles of lesson 10.

Worked problem

A subclass that breaks substitution

Construction crews use some NS-4 units as TemporaryNode objects at work zones. Someone writes class TemporaryNode extends RadarNode and overrides reaim(lanes) to throw UnsupportedOperationException, because temporary nodes are always set up for exactly two lanes and must not be re-aimed remotely. The operator console calls node.reaim(...) on every selected node after a road layout change. What goes wrong, and how should it be designed?

Show solution
  1. The console was written against RadarNode, whose documentation promises that reaim changes the watched lanes. TemporaryNode accepts the call but throws, so a batch update that includes one temporary node aborts halfway, leaving some nodes re-aimed and others not.
  2. The subclass promises less than its superclass, so it violates LSP, even though the code compiles and every class's own tests pass.
  3. Fix 1: model the capability explicitly. Only nodes that can be re-aimed implement an interface Reaimable (lesson 7), and the console asks for that interface, so temporary nodes are never offered the operation.
  4. Fix 2: make the restriction part of the superclass's contract, with a method canReaim() that callers check, documented in RadarNode from the start.
  5. Either way, the design moves the difference into the types or the contract instead of hiding it in a surprise exception.

Answer: An override that throws to refuse an inherited operation is a warning sign of an is-a relationship that is not really true. Model capabilities with interfaces or composition instead.

The classic textbook version of this mistake is Square extends Rectangle. Mathematically a square is a rectangle, but if Rectangle has independent setWidth and setHeight methods, a Square cannot honor them without breaking its own invariant, and code that sets a rectangle's width and expects the height to stay unchanged will fail on a square. Is-a in everyday language is not the same as substitutability in code; behavior, not taxonomy, decides.

The fragile base class problem

Inheritance couples a subclass tightly to the internals of its superclass. A subclass that overrides one method may depend, without knowing it, on which other superclass methods call that method. If a later version of the superclass changes that internal detail, the subclass breaks even though its own code never changed. This is called the fragile base class problem. A famous illustration is a subclass of HashSet that counts how many elements have been added by overriding both add and addAll; because HashSet's addAll happens to call add internally, every element added through addAll is counted twice. The subclass's correctness depends on an implementation detail that HashSet never promised. Composition, wrapping a set and delegating to it, avoids the problem, which is one reason lesson 10 recommends composition over inheritance.

When inheritance is the right choice

After all these warnings, it is worth stating plainly when inheritance works well. It works when the subclasses are genuinely specializations that keep every promise of the superclass; when the superclass was designed and documented for extension, usually as an abstract class like the one in lesson 7; when the hierarchy is shallow, typically one or two levels; and when the variation between subclasses is in behavior that the superclass deliberately leaves open, such as how length is estimated. The node hierarchy meets all four tests. Framework code often uses inheritance this way: a testing framework provides a base test class, and a graphical toolkit provides a base widget class whose drawing method you override. In each case the base class author has decided exactly which methods are meant to be overridden and has documented what an override must do.

Inheritance works badly when it is used only to borrow code, when the subclass must disable or contradict inherited behavior, or when the hierarchy tries to capture several independent ways of varying at once. If nodes varied by sensor type, by power source and by communication link, a subclass for every combination would need dozens of classes; composing a node from a sensor object, a power object and a link object needs only one class per option. That trade-off is the theme of lesson 10.

Inheritance in Python and C++

Python writes the superclass in parentheses after the class name and calls inherited methods with super(). Python allows multiple inheritance, in which a class has several superclasses; it resolves which version of a method to use with a rule called the method resolution order, MRO, which you can inspect with ClassName.__mro__. Multiple inheritance is used most safely for mixins, small classes that add one capability, such as JSON serialization, to many unrelated classes. C++ also supports multiple inheritance and adds access specifiers on the inheritance itself; public inheritance models is-a. A C++ base class that will be used through pointers must declare a virtual destructor, a point lesson 6 explains.

class CounterNode:
    def __init__(self, node_id, lanes):
        if not 1 <= node_id <= 65535:
            raise ValueError("bad id")
        self.node_id, self.lanes = node_id, tuple(lanes)
    def estimate_length(self, det):
        return det["length_cm"]                       # default: measured directly
    def describe(self):
        return f"node {self.node_id} lanes {self.lanes}"

class LoopNode(CounterNode):
    def __init__(self, node_id, lanes, spacing_cm):
        super().__init__(node_id, lanes)              # initialize the base part first
        self.spacing_cm = spacing_cm
    def estimate_length(self, det):                   # override: length from loop timing
        speed = self.spacing_cm / 100 / det["gap_s"]
        return round(speed * det["occupancy_s"] * 100) - 180
    def describe(self):
        return "NS-3 loop " + super().describe()       # extend, don't copy

class RadarNode(CounterNode):
    def describe(self):
        return "NS-4 radar " + super().describe()

class JsonMixin:                                      # a mixin adds one capability
    def to_json(self):
        import json
        return json.dumps({"type": type(self).__name__, "id": self.node_id})

class CameraNode(JsonMixin, CounterNode):
    def describe(self):
        return "NS-5 camera " + super().describe()

nodes = [LoopNode(311, [1, 2], 400), RadarNode(17, [1, 2, 3, 4]), CameraNode(902, [1, 2])]
det = {"length_cm": 455, "gap_s": 0.25, "occupancy_s": 0.40}
for n in nodes:
    print(f"{n.describe():<34} length {n.estimate_length(det)} cm")
print(nodes[2].to_json())
print("is a CounterNode:", [isinstance(n, CounterNode) for n in nodes])
print("CameraNode MRO:", [c.__name__ for c in CameraNode.__mro__])
Output
NS-3 loop node 311 lanes (1, 2)    length 460 cm
NS-4 radar node 17 lanes (1, 2, 3, 4) length 455 cm
NS-5 camera node 902 lanes (1, 2)  length 455 cm
{"type": "CameraNode", "id": 902}
is a CounterNode: [True, True, True]
CameraNode MRO: ['CameraNode', 'JsonMixin', 'CounterNode', 'object']

The loop node's estimate in the output deserves a check. With loops 400 cm apart and a 0.25 s gap between triggering the first and second loop, the speed is 16 m/s, about 58 km/h. A vehicle that keeps one loop occupied for 0.40 s at that speed covers 640 cm during the occupancy, which is its own length plus the effective length of the loop; subtracting 180 cm of loop gives 460 cm, a typical car. The same detection measured directly by a radar node is 455 cm. Inheritance let each model compute length its own way while the code that loops over the nodes stayed the same.

Common mistakes

  • MistakeUsing inheritance just to reuse code from an unrelated class.

    InsteadInherit only for true, substitutable is-a relationships; reuse code through composition and delegation.

  • MistakeOverriding a method to throw an exception because the subclass cannot support it.

    InsteadThat breaks substitution; model the capability with an interface or redesign the hierarchy.

  • MistakeMaking superclass fields protected for convenience.

    InsteadKeep them private and give subclasses protected methods, so the superclass can still guard its invariants.

  • MistakeLeaving off @Override.

    InsteadAlways annotate overrides so a misspelt name or wrong parameter type is caught by the compiler.

  • MistakeBuilding deep hierarchies several levels tall.

    InsteadDeep trees are fragile and hard to understand; keep hierarchies shallow and prefer composition when in doubt.

Summary and key terms

  • Inheritance defines a subclass that extends a superclass, inheriting its members, adding new ones and overriding methods.
  • Java has single inheritance of classes; constructors chain through super(...), and every class descends from Object.
  • Use @Override, keep superclass fields private, and use final to prevent overriding or extension where a class is not designed for it.
  • The Liskov substitution principle: subclass objects must work anywhere superclass objects do, without demanding more or promising less.
  • Inheritance couples subclasses to superclass internals (the fragile base class problem); Python and C++ allow multiple inheritance, best used for mixins.
Inheritance
Defining a class as an extension of another, inheriting its fields and methods.
Subclass
A class that extends another class; also called a derived class.
Override
A subclass method that replaces an inherited method with the same signature.
Liskov substitution principle
Objects of a subclass must be substitutable for objects of the superclass without breaking correctness.
Fragile base class problem
Changes inside a superclass breaking subclasses that depended on its implementation details.
Mixin
A small class that adds one capability to other classes through multiple inheritance.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Say whether inheritance fits: Lane and Station; CameraNode and CounterNode; Truck and Vehicle; Logger and FileWriter.

    Show answer

    Lane/Station: no (composition). CameraNode/CounterNode: yes. Truck/Vehicle: yes, if substitutable. Logger/FileWriter: usually no; a logger has or uses a writer.

  2. What happens if a subclass constructor does not call super(...) and the superclass has no no-argument constructor?

    Show answer

    Compilation fails, because the compiler's implicit super() call has nothing to call.

  3. Why does the counting HashSet subclass double-count elements added with addAll?

    Show answer

    HashSet.addAll calls add internally, so both overrides count each element.

  4. Loops are 400 cm apart and the gap is 0.20 s. Estimate the speed in km/h.

    Show answer

    4.0 m / 0.20 s = 20 m/s = 72 km/h.

  5. What does Python's MRO determine?

    Show answer

    The order in which superclasses are searched for a method or attribute.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz