Inheritance and Polymorphism in Object-Oriented Programming
Learning Objectives
By the end of this page, you will be able to:
- Define inheritance and explain the "is-a" relationship it models
- Identify and give examples of single, multilevel, hierarchical, and multiple inheritance
- Explain why Java disallows multiple class inheritance but C++ allows it
- Distinguish compile-time polymorphism (overloading) from runtime polymorphism (overriding)
- Trace how dynamic method dispatch resolves an overridden method call at runtime
- Recognize when inheritance is being misused in place of composition
Quick Answer
Inheritance lets a new class (a subclass) reuse and extend the attributes and methods of an existing class (its superclass), avoiding duplicated code across related types. Polymorphism lets code call the same method name on different objects and get behavior specific to each object's actual class — it comes in two forms: compile-time (method overloading, resolved by parameter types) and runtime (method overriding, resolved by the object's real type during execution). Together they let you write a function that operates on a general type (like Animal) while automatically getting the correct, specialized behavior for whatever concrete subclass (Dog, Cat) is actually passed in — this is what makes large OOP codebases extensible without constant rewriting.
Core Content
1. What Inheritance Is
Definition: Inheritance is a mechanism where a new class (subclass/derived class) acquires the fields and methods of an existing class (superclass/base class), and can add new members or override inherited ones.
Explanation: Inheritance should model a genuine "is-a" relationship — a Dog is an Animal. When that relationship holds, the subclass automatically gets everything the superclass already defines, so you only write behavior that's actually new or different. In Java, a class extends exactly one other class using extends; in C++, class Dog : public Animal does the same and can list multiple base classes; in Python, inheritance is written as class Dog(Animal):.
Simple Example (Java):
class Animal {
void eat() {
System.out.println("This animal eats.");
}
}
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
Dog myDog = new Dog();
myDog.eat(); // inherited from Animal
myDog.bark(); // defined in Dog
Real-World Example: A "smartphone" is a specialized kind of "phone" — it inherits the basic ability to make calls, but adds capabilities (apps, cameras, internet browsing) that a plain phone doesn't have. You don't redefine "making calls" for every new phone model; you inherit it.
Why It Matters: Inheritance is the primary tool for eliminating duplicated logic across a family of related classes, and it establishes the type hierarchy that runtime polymorphism relies on.
Common Misunderstanding: Students think any two classes that happen to share a few methods should be linked with inheritance. If the relationship isn't truly "is-a" — for example, a Car and an Engine sharing a start() idea — forcing inheritance creates a confusing hierarchy; a "has-a" relationship should use composition instead.
2. Types of Inheritance
Definition: Inheritance can be arranged in different structural patterns: single, multilevel, hierarchical, multiple, and hybrid.
Explanation:
- Single inheritance: one subclass, one superclass (
Dog extends Animal). - Multilevel inheritance: a chain, where a class inherits from a class that itself inherits from another (
Dog extends Mammal extends Animal). - Hierarchical inheritance: one superclass is inherited by multiple, unrelated subclasses (
DogandCatboth extendAnimal). - Multiple inheritance: a class inherits directly from more than one superclass. C++ supports this directly; Java does not allow it for classes (to avoid the "diamond problem," where two parent classes define a conflicting method) but achieves similar flexibility through interfaces, which can be implemented in any combination.
- Hybrid inheritance: a combination of the above patterns in one design, often built using interfaces in Java to sidestep the multiple-class-inheritance restriction.
Simple Example (Java — multilevel inheritance):
class Animal {
void eat() { System.out.println("This animal eats."); }
}
class Mammal extends Animal {
void walk() { System.out.println("This mammal walks."); }
}
class Dog extends Mammal {
void bark() { System.out.println("The dog barks."); }
}
Dog myDog = new Dog();
myDog.eat(); // from Animal
myDog.walk(); // from Mammal
myDog.bark(); // from Dog
Simple Example (C++ — multiple inheritance):
class Swimmer {
public:
void swim() { cout << "Swimming" << endl; }
};
class Flyer {
public:
void fly() { cout << "Flying" << endl; }
};
class Duck : public Swimmer, public Flyer {
// Duck inherits from two unrelated base classes at once
};
Real-World Example: A university's organization chart is hierarchical inheritance in action: "Department" is the shared parent structure, and "Computer Science," "Physics," and "Mathematics" departments all inherit the same base structure (a head, a budget, a set of courses) while adding their own specifics.
Why It Matters: Recognizing which inheritance pattern a design calls for prevents both under-modeling (duplicating code across siblings) and over-modeling (a deep, fragile inheritance chain that's hard to reason about).
Common Misunderstanding: Students assume Java doesn't support "multiple inheritance" in any form. It disallows multiple class inheritance specifically to avoid ambiguity when two parents define the same method — but a Java class can implement any number of interfaces, which is a form of multiple inheritance of type (though not of implementation, prior to default methods).
3. Compile-Time Polymorphism (Method Overloading)
Definition: Method overloading lets a class define multiple methods with the same name but different parameter lists (different number, order, or types of parameters); the compiler picks the correct one based on the arguments at the call site.
Explanation: Because the compiler decides which overloaded method to call by matching argument types before the program ever runs, this is called compile-time (or static) polymorphism. It has nothing to do with inheritance — all overloaded versions typically live in the same class.
Simple Example (Java):
class MathOperations {
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
double add(double a, double b) {
return a + b;
}
}
MathOperations mo = new MathOperations();
System.out.println(mo.add(5, 3)); // calls the 2-int version
System.out.println(mo.add(5, 3, 7)); // calls the 3-int version
System.out.println(mo.add(5.5, 3.2)); // calls the double version
Real-World Example: A single vending machine slot labeled "pay" accepts coins, cards, or phone taps — the machine picks the right internal process based on what you insert, even though you press the same conceptual "pay" action.
Why It Matters: Overloading lets an API offer several convenient ways to call the same logical operation (e.g., add(int, int) vs add(double, double)) without inventing a different method name for each variant.
Common Misunderstanding: Students think changing only the return type of a method (keeping the same name and parameters) counts as overloading. It doesn't — Java and C++ both require the parameter list to differ; a return-type-only difference is a compile error.
4. Runtime Polymorphism (Method Overriding)
Definition: Method overriding lets a subclass provide its own implementation of a method already defined in its superclass, using the same method signature; the version that actually runs is chosen based on the object's real type at execution time.
Explanation: This is called dynamic dispatch: even if a variable is declared with the superclass type, calling a method on it looks up the object's actual class at runtime and executes that class's version. This is what allows a single loop to call animal.speak() on a mixed collection of Dog and Cat objects and get the correct sound for each, without any if/else type-checking.
Simple Example (Java):
class Animal {
void makeSound() {
System.out.println("Animal makes a sound.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks.");
}
}
Animal myAnimal = new Dog(); // declared as Animal, but actually a Dog
myAnimal.makeSound(); // Output: Dog barks. (resolved at runtime)
Simple Example (Python):
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
for animal in [Dog(), Cat()]:
print(animal.speak()) # each object's own speak() runs
Real-World Example: Pressing "play" on a universal remote works whether it's pointed at a TV, a sound system, or a streaming box — the same button press dispatches to whichever device is actually in front of it, and each device "plays" in its own way.
Why It Matters: Runtime polymorphism is what lets you add a brand-new subclass to a system later without modifying any of the existing code that already works with the superclass type.
Common Misunderstanding: Students think declaring a variable's type as the superclass (Animal myAnimal = new Dog();) means the superclass's method will run. It's the opposite — the declared type only restricts which methods you're allowed to call at compile time, but the actual object's class always determines which overridden version executes.
Key Terms
| Term | Definition | Context/Related Concepts |
|---|---|---|
| Inheritance | A subclass acquiring the members of a superclass | Models "is-a" relationships |
| Superclass/Base class | The class being inherited from | Also called parent class |
| Subclass/Derived class | The class that inherits from another | Also called child class |
| Single inheritance | One subclass inherits from exactly one superclass | Simplest inheritance form |
| Multilevel inheritance | A chain of inheritance across three or more classes | Dog extends Mammal extends Animal |
| Hierarchical inheritance | Multiple subclasses inherit from one shared superclass | Dog and Cat both extend Animal |
| Multiple inheritance | A class inherits from more than one superclass | Allowed in C++; not for classes in Java (diamond problem) |
| Method overloading | Same method name, different parameter lists, same class | Compile-time polymorphism |
| Method overriding | Subclass redefines a superclass method with the same signature | Runtime polymorphism |
| Dynamic dispatch | Runtime lookup of which overridden method to execute, based on actual object type | Mechanism behind runtime polymorphism |
| Diamond problem | Ambiguity when a class inherits the same method from two parents via multiple inheritance | Reason Java restricts multiple class inheritance |
Common Mistakes
Misconception 1: "Overloading and overriding are just two names for the same idea." Why It's Wrong: They differ in where they occur, how they're resolved, and what they require. Correct Understanding: Overloading happens within one class with differing parameter lists and is resolved at compile time; overriding happens across a superclass/subclass pair with an identical signature and is resolved at runtime based on the object's actual type.
Misconception 2: "If a variable is declared as the superclass type, calling a method on it runs the superclass's version."
Why It's Wrong: This confuses the declared (compile-time) type with the actual (runtime) type of the object.
Correct Understanding: For overridden instance methods, Java, C++ (with virtual), and Python always execute the method belonging to the object's real class, regardless of the variable's declared type.
Misconception 3: "Inheritance should be used any time two classes share behavior, to avoid duplicating code." Why It's Wrong: Inheritance implies an "is-a" relationship; forcing it onto classes that merely share incidental behavior creates a fragile hierarchy where subclasses inherit things that don't apply to them. Correct Understanding: Use inheritance only when the subclass genuinely is a more specific version of the superclass. When one class simply needs the functionality of another without being a specialized version of it, use composition (holding a reference to the other class) instead.
Comparison and Connections
| Concept A | Concept B | Key Difference |
|---|---|---|
| Method overloading | Method overriding | Overloading: same class, different parameters, compile-time resolution. Overriding: subclass, same signature, runtime resolution |
| Single inheritance | Multiple inheritance | Single: one direct parent. Multiple: more than one direct parent; C++ allows it for classes, Java only via interfaces |
| Inheritance | Composition | Inheritance reuses via an "is-a" hierarchy; composition reuses via a "has-a" reference to another object |
| Compile-time polymorphism | Runtime polymorphism | Compile-time is resolved by the compiler using argument types; runtime is resolved by the JVM/interpreter using the object's actual class |
| Abstract method | Overridden method | An abstract method has no body and must be implemented by a subclass; an overridden method replaces a superclass method that already has a body |
Practice Questions
Recall
-
What is the difference between a superclass and a subclass? Answer: A superclass (base/parent class) is the class being inherited from; a subclass (derived/child class) inherits its members and can add or override behavior.
-
Which type of polymorphism is resolved at compile time, and which is resolved at runtime? Answer: Method overloading is resolved at compile time; method overriding is resolved at runtime.
Understanding
-
Explain why Java does not allow a class to extend two other classes directly, while C++ does. Answer: Allowing multiple class inheritance creates the "diamond problem" — if two parent classes both define a method with the same signature, it's ambiguous which one the subclass should inherit. Java sidesteps this by restricting classes to a single superclass and instead allowing multiple interface implementation, where conflicts are handled differently (or must be resolved explicitly). C++ allows multiple inheritance but requires the programmer to resolve any resulting ambiguity manually.
-
Why does calling an overridden method on a superclass-typed reference still execute the subclass's version? Answer: Because instance method calls use dynamic dispatch — the runtime looks at the object's actual class (not the variable's declared type) to decide which version of the method to run. The declared type only restricts which method names are legal to call at compile time.
Application
-
Design a class hierarchy for
Employee,Manager, andEngineer, where every employee has acalculateSalary()method but managers and engineers compute it differently. Which OOP mechanism should you use, and how would you structure it? Answer: Use inheritance with runtime polymorphism:ManagerandEngineerboth extendEmployeeand overridecalculateSalary()with their own formula. Code that processes a list ofEmployeeobjects can callcalculateSalary()uniformly and get the correct calculation for each actual subtype. -
Write a Java class
Printerwith two overloadedprintmethods: one that takes aStringand one that takes anint. Answer:class Printer {void print(String text) {System.out.println("Text: " + text);}void print(int number) {System.out.println("Number: " + number);}}
Analysis
-
A student overrides
toString()in a subclass but changes its return type fromStringtoObject. Why does this fail to compile, and how does this differ from valid overloading? Answer: A valid override must keep the exact same signature (or a covariant return type that is a subtype, not a supertype) as the method it overrides — changing the return type to a broader type likeObjectviolates the override contract and the compiler rejects it. This is unrelated to overloading, which requires a different parameter list, not a modified return type, and lives within a single class rather than across a superclass/subclass pair. -
Compare adding a new animal type to a polymorphic hierarchy (create a
Birdsubclass overridingspeak()) versus adding a newif isinstance(animal, Bird)branch inside an existingdescribe_animal()function. Which approach is more maintainable as the number of animal types grows, and why? Answer: The polymorphic subclass approach is more maintainable — addingBirdrequires writing exactly one new class, and every existing piece of code that callsanimal.speak()automatically works withBirdtoo. Theisinstancebranch approach requires locating and editing every function that performs this kind of type-checking, and it's easy to forget one, leading to inconsistent behavior for the new type.
FAQ
Q: Can a subclass access private members of its superclass directly?
A: No. Private members are accessible only within the class that declares them. A subclass can access inherited protected or public members directly, but must go through inherited public/protected methods (like getters) to interact with private superclass fields.
Q: What happens if a subclass doesn't override a method — does the superclass version simply run? A: Yes. If a subclass doesn't provide its own version of a method, calling it on a subclass instance runs the superclass's implementation unchanged, because the subclass "inherited" it as-is.
Q: Does Python support method overloading like Java does?
A: Not directly — defining two methods with the same name in a Python class just makes the second one overwrite the first. Python developers typically use default parameter values or *args/**kwargs to achieve similar flexibility.
Q: Is it possible to prevent a class from being inherited, or a method from being overridden?
A: Yes. In Java, final on a class prevents inheritance and final on a method prevents overriding. In C++, the final specifier does the same for classes and virtual methods.
Q: Why do we need the @Override annotation in Java if it's optional?
A: It's optional for the compiler to run the code, but it's a safety check — if you misspell a method name or use the wrong parameter types, @Override causes a compile error instead of silently creating an unrelated overloaded method that never gets called polymorphically.
Quick Revision
- Inheritance lets a subclass reuse and extend a superclass's members; it should model a true "is-a" relationship.
- Single, multilevel, hierarchical, multiple, and hybrid inheritance are structural patterns for arranging class relationships.
- Java disallows multiple class inheritance (diamond problem) but allows implementing multiple interfaces; C++ allows multiple class inheritance directly.
- Method overloading = same name, different parameters, same class, resolved at compile time.
- Method overriding = same signature, subclass replaces superclass behavior, resolved at runtime via dynamic dispatch.
- A superclass-typed reference still runs the subclass's overridden method — the actual object type governs, not the declared type.
final(Java) can prevent a class from being subclassed or a method from being overridden.- Prefer composition over inheritance when the relationship is "has-a" rather than "is-a."
@Overrideis optional but catches signature mistakes at compile time.- Python has no built-in overloading; default arguments or
*argssubstitute for it. - Polymorphism lets new subclasses integrate with existing code with zero changes to that code.
Related Topics
Prerequisites
- Classes and objects
- OOP principles and concepts (the four pillars)
Related Topics
- Encapsulation and abstraction
- Interfaces and abstract classes
- Access modifiers
Next Topics
- Encapsulation and abstraction in depth
- Exception handling
- Design patterns