4 Inheritance and Class Hierarchies

Learn how inheritance organizes related classes, enables polymorphism, and should be designed around genuine specialization rather than convenient code reuse.

From General Types to Specialized Types

lets a specialized class reuse and extend a more general class. The general class is the , base class, or parent class; the specialized class is the , derived class, or child class.

The central design test is the . If a Dog is an Animal, then Dog may inherit from Animal. A dog can use behavior such as eat() inherited from Animal and can add behavior such as fetch(). A hierarchy may continue through multiple levels, such as Animal to Mammal to Dog.

A normally has one direct in languages such as Java and C#, while a can have many subclasses. Inherited members remain available through the hierarchy subject to access-control and language rules. Constructors are generally not inherited, although a can invoke a constructor.

Takeaway: Use to express a genuine specialization in which the child type can meaningfully be treated as the parent type.

Reusing Behavior and Defining Abstractions

A can hold behavior shared by several related subclasses, allowing common code to be written once. For example, a Vehicle class might define start(), while an ElectricCar reuses start() and adds chargeBattery().

This form of reuse is valuable when the shared behavior is genuinely common and stable. The should not contain features that make sense for only one specialized type. Code reuse by itself is not enough to justify an relationship.

An is useful when the general concept is meaningful but should not be created directly. For example, an abstract Shape class can declare an area() operation, while a concrete Circle class supplies the implementation using its radius. The abstract class may contain shared implementation and can require each concrete to provide essential operations.

Takeaway: Put stable, conceptually shared behavior in a , and use abstraction when subclasses must complete a common contract.

and Run-Time Behavior

A overrides an inherited method when it provides a new implementation with the same method signature. If Animal defines speak() and Dog overrides it, a dog can produce Bark instead of the general animal sound.

A may also extend the original behavior. For example, LoudDog can call the implementation through super.speak() and then add its own output. The corresponding keyword in C# is base.

appears when a variable uses a type but refers to a object. In the expression Animal animal = new Dog();, calling animal.speak() selects the overridden Dog.speak() implementation at run time. This lets a method or collection work with general Animal values while preserving specialized behavior.

is reliable when each honors the behavior promised by the . Similarly, code can request area() from every Shape without needing separate calling logic for each concrete shape.

Takeaway: supplies specialized behavior, while lets general code select that behavior through a common type.

Choosing or

creates dependencies between a and all of its descendants. A change to inherited state, method behavior, construction, or access levels can affect many subclasses. Good design therefore requires more than a convenient way to copy code.

Use when:

  • The genuinely is a specialized version of the .

  • The 's public behavior makes sense for every .

  • Shared behavior is stable and belongs conceptually in the .

  • Every can honor the expectations established by the .

Avoid when:

  • The relationship is really has-a or uses. A Car has an Engine; an Engine is not a kind of Car.

  • A would need to disable, ignore, or reject inherited operations.

  • The exposes implementation details that subclasses must depend on.

  • Code reuse is the only reason for the relationship.

is often preferable when one object contains or uses another. It can provide looser coupling and make independent behaviors easier to combine.

Takeaway: Choose for a stable conceptual relationship, not merely for implementation reuse.

Managing Hierarchy Risk

A has many levels between a concrete class and its root. Such a structure can make important fields and methods difficult to locate, because behavior may be defined several levels above the class being inspected.

Deep hierarchies can also create fragile dependencies: a change near the top may alter many descendants. Several overrides of the same method can make execution order difficult to determine, and each level may impose requirements on construction and initialization. A may also inherit operations that do not fit its meaning, weakening substitutability.

A shallow hierarchy with clear responsibilities is usually easier to understand, test, and maintain. When extension is not part of the design, languages can provide restrictions such as Java's final or C#'s sealed to prevent further of selected classes or methods.

Takeaway: Keep hierarchies shallow when possible, make responsibilities clear, and protect important invariants when subclasses should not extend a type.