Inheritance

On this page 7

Inheritance is the mechanism in object-oriented programming by which a subclass derives the fields and methods of a superclass, reusing its implementation and extending or overriding parts of it. The relationship it models is is-a: a SavingsAccount is an Account, so it can be used anywhere an Account is expected.

Inheritance combines two things that are often confused: reuse of implementation and creation of a subtype. It enables polymorphism through overriding, but it also creates the tightest form of coupling between classes, which is why modern design guidance treats it as a precise tool rather than a default way to share code.

How inheritance works at runtime

When a class inherits from another, every object of the subclass contains the state of the superclass plus its own additional fields. Methods that the subclass does not redefine are looked up in the superclass, while methods it overrides replace the parent behavior for objects of the subclass.

In statically typed languages such as Java, C# and C++, overridable methods are dispatched through a virtual method table, a per-class array of function pointers that the runtime consults to call the correct implementation. Dynamic languages such as Python and Ruby walk a method resolution order, the ordered list of classes searched for an attribute. In both cases the call site does not need to know the concrete class, which is the core of runtime polymorphism.

Constructors are not inherited in most languages; instead a subclass constructor must initialize the parent part first, explicitly through super(...) or implicitly through a default constructor. Calling overridable methods from a constructor is a known hazard, because the subclass state is not yet initialized when the parent constructor runs.

Forms of inheritance

Languages differ in which inheritance forms they allow, mainly because multiple inheritance of implementation creates ambiguity. The table summarizes the common forms and how mainstream languages handle them.

FormStructureLanguage supportMain concern
Single inheritanceOne parent per classJava, C#, Kotlin, Swift classesDeep chains become rigid
Multiple inheritanceSeveral parent classesC++, PythonDiamond problem and name clashes
Interface inheritanceInherits contracts, not codeJava, C#, Go implicitlyImplementation must be repeated or delegated
Mixins and traitsReusable method bundlesRuby modules, Scala and Rust traits, PHP traitsOrdering and conflict rules
Prototypal inheritanceObjects delegate to other objectsJavaScriptShared mutable prototype state
Inheritance forms and language support

The diamond problem arises when a class inherits from two classes that share a common ancestor: it becomes unclear which version of an inherited method applies. C++ resolves it with virtual inheritance, while Python uses the deterministic C3 linearization to order the classes. Languages such as Java avoid the issue for state by permitting multiple inheritance only of interfaces.

Inheritance is one of several mechanisms for relating classes, and choosing well requires seeing what each mechanism actually shares. The comparison below positions it against its closest neighbors.

MechanismWhat is sharedRelationshipCoupling
InheritanceImplementation and typeIs-aHigh
Interface implementationType contract onlyCan-doLow
CompositionBehavior through a held objectHas-aLow to moderate
DelegationCalls forwarded to a helperUses-aLow
GenericsAlgorithm over many typesWorks-withLow
Inheritance compared with related mechanisms

The guideline favor composition over inheritance expresses a practical observation: composition lets behavior be swapped at runtime and keeps classes independent, while inheritance fixes the relationship at compile time. Inheritance remains the right choice when a genuine subtype relationship exists and the parent class was designed for extension.

The Liskov substitution principle

Correct inheritance obeys the Liskov substitution principle: any code written against the superclass must work unchanged with any subclass. A subclass may weaken preconditions and strengthen postconditions, but it must not demand more from callers or promise less to them.

The classic violation is a Square that inherits from Rectangle. Mathematically a square is a rectangle, yet setting the width of a mutable square must also change its height, which breaks callers that expect width and height to vary independently. The lesson is that inheritance should follow behavioral compatibility, not only real-world taxonomy.

When to use inheritance and when to avoid it

Inheritance works best for shallow, stable hierarchies in which the parent class defines a clear extension point. The decision table reflects common practice in professional codebases.

SituationRecommended approachReason
Framework defines a base class with hooksInherit and override hooksDesigned extension points
Several classes need one shared helperCompositionAvoids an artificial is-a link
Unrelated types need common behaviorInterface plus delegationKeeps hierarchies independent
Algorithm skeleton with varying stepsTemplate method patternParent controls the sequence
Third-party class not built for extensionWrap it insteadAvoids the fragile base class problem
Choosing inheritance or an alternative

The fragile base class problem is the main risk: a harmless-looking change in a parent, such as calling one public method from another, can silently break subclasses that override those methods. Hierarchies deeper than two or three levels also make behavior hard to trace. Languages respond with explicit controls, such as final in Java, sealed in C# and Kotlin, and classes that are closed by default in Kotlin.

A plain-language summary

Inheritance lets a programmer say that one kind of thing is a special version of another. If the program already knows what a vehicle can do, it can describe a truck by saying it is a vehicle plus a cargo bed, instead of writing everything again.

The benefit is reuse, but there is a catch. The truck now depends on every detail of the vehicle, so a change to the vehicle can affect the truck in surprising ways. Inheritance is powerful when the family relationship is real and risky when it is used only to borrow code.

An example from practice

A logistics company builds a shipment tracking system with an abstract Carrier class. It defines a fixed workflow, which authenticates, requests a label and parses tracking events, and leaves three abstract methods for each carrier to implement. Separate subclasses for a national postal service and two courier companies override only those three hooks.

When a new courier is added, a developer writes one subclass of about 200 lines, and the rest of the system uses it through the Carrier type without changes. Logging and retry logic, which every carrier needs, are injected through composition rather than added to the base class. The result is a stable two-level hierarchy that has absorbed new carriers for years without base class rewrites.