Inheritance and Polymorphism

Topics Covered

Inheritance and the Is-A Relationship

Why Inheritance Exists

The Is-A Test

What Gets Inherited

Single vs Multiple Inheritance

Method Overriding and Super

How Overriding Works

The Role of Super

Overriding in Java

Overriding vs Overloading

When NOT to Override

Polymorphism in Action

The Power of a Common Interface

Polymorphism Through Inheritance

Real-World Polymorphism: Payment Processing

Collections of Mixed Types

The isinstance Smell

Composition vs Inheritance

The Problem with Inheritance Hierarchies

Composition: Has-A Instead of Is-A

When Inheritance Wins

The Decision Framework

When Inheritance Goes Wrong

The Fragile Base Class Problem

Deep Hierarchies and the God Class at the Top

Forced Categorization

Inheritance for Code Reuse (The Wrong Reason)

Signs You Should Refactor Away from Inheritance

Practice Problems

Inheritance is the most overused feature in OOP. Before you learn the mechanics, you need to understand why that statement is true: and why inheritance still matters when used correctly.

The core idea is simple: a child class extends a parent class, inheriting all of the parent's fields and methods. You write the shared behavior once in the parent, and every child gets it for free. This is the "is-a" relationship. A Dog is-a Animal. A SavingsAccount is-a BankAccount. A Manager is-a Employee.

Inheritance hierarchy with parent behavior flowing to children

Why Inheritance Exists

Without inheritance, you would copy-paste shared behavior into every class. Ten animal types that all need a name field and an eat() method means ten copies of the same code. When you fix a bug in eat(), you fix it in ten places. Inheritance eliminates this duplication by centralizing shared behavior in a parent class.

python
1class Animal:
2    def __init__(self, name):
3        self.name = name
4
5    def eat(self):
6        return f"{self.name} is eating"
7
8class Dog(Animal):
9    def speak(self):
10        return "Woof"
11
12class Cat(Animal):
13    def speak(self):
14        return "Meow"

Dog and Cat both inherit name and eat() from Animal. They add their own speak() behavior. If you fix a bug in eat(), every animal type gets the fix automatically.

The Is-A Test

Before creating an inheritance relationship, apply the is-a test rigorously. Ask: "Is every instance of the child truly an instance of the parent, in every context?" A Circle is-a Shape: yes. A Penguin is-a Bird: tricky, because Bird might have a fly() method that penguins cannot use.

The is-a test is not about real-world taxonomy. It is about behavioral compatibility. In the real world, a square is-a rectangle. In code, Square breaks Rectangle's contract because setting width on a square also changes height. The lesson: inheritance models behavioral relationships, not biological or mathematical ones.

What Gets Inherited

A child class inherits everything from its parent:

  • Fields: instance variables defined in the parent become part of the child
  • Methods: all public and protected methods are available on the child
  • Constructors: the parent's constructor must be called (explicitly or implicitly) before the child's constructor runs

What does not get inherited depends on the language. In Java, private fields exist in the child object's memory but cannot be accessed directly: you use getters. In Python, name-mangled attributes (prefixed with __) are technically accessible but conventionally private.

java
1class Employee {
2    private String name;
3    private double baseSalary;
4
5    public Employee(String name, double baseSalary) {
6        this.name = name;
7        this.baseSalary = baseSalary;
8    }
9
10    public double getSalary() {
11        return baseSalary;
12    }
13
14    public String getName() {
15        return name;
16    }
17}
18
19class Manager extends Employee {
20    private double bonus;
21
22    public Manager(String name, double baseSalary, double bonus) {
23        super(name, baseSalary);
24        this.bonus = bonus;
25    }
26
27    @Override
28    public double getSalary() {
29        return super.getSalary() + bonus;
30    }
31}

Notice that Manager calls super(name, baseSalary) to initialize the parent's fields. The parent's constructor runs first, setting up the inherited state. Then the child's constructor adds its own state (bonus). This ordering is mandatory: you cannot use inherited fields before they are initialized.

Common Pitfall

Inheritance creates tight coupling between parent and child. Every change to the parent's public or protected interface affects every child class. A method added to Animal affects Dog, Cat, Bird, and every other subclass. Before you create an inheritance hierarchy, ask yourself: will the parent's behavior remain stable? If the parent changes frequently, you are propagating instability to every child.

Single vs Multiple Inheritance

Most languages allow single inheritance only: a class extends exactly one parent. Java, C#, and Ruby follow this model. Python and C++ allow multiple inheritance, where a class can extend two or more parents.

Multiple inheritance introduces the diamond problem. If class D extends both B and C, and both B and C extend A, which version of A's methods does D get? Python resolves this with Method Resolution Order (MRO), which linearizes the hierarchy into a predictable lookup chain. C++ requires explicit disambiguation. Java avoids the problem entirely by limiting inheritance to one class while allowing multiple interfaces.

The diamond problem is not just academic. It causes real bugs in production code when two parent classes define methods with the same name but different behavior. This is one of the reasons most modern language designers restrict or discourage multiple inheritance.