OOP - Inheritance and Polymorphism
Understand inheritance in Java with extends/super, method overriding, and dynamic dispatch through practical examples.
Why this step matters
Inheritance and polymorphism are central to object-oriented Java. Used correctly, they let you model shared behavior and write flexible code that works with abstractions instead of concrete classes.
Inheritance: extends
Inheritance means a class can reuse and specialize another class.
class Animal {
protected String name;
Animal(String name) {
this.name = name;
}
void speak() {
System.out.println(name + " makes a sound");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
}
Dog inherits fields and methods from Animal.
When inheritance fits
- there is a true “is-a” relationship (
Dog is an Animal) - child classes share stable common behavior from a parent
Avoid inheritance only for code reuse when there is no domain relationship.
super: accessing parent behavior
super is used to interact with the parent class.
Constructor chaining
class Dog extends Animal {
Dog(String name) {
super(name); // call parent constructor
}
}
Calling parent methods
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void speak() {
super.speak();
System.out.println(name + " barks");
}
}
Method overriding
Overriding means redefining inherited behavior in a subclass with the same signature.
class Animal {
void speak() {
System.out.println("generic sound");
}
}
class Cat extends Animal {
@Override
void speak() {
System.out.println("meow");
}
}
Overriding rules (key points)
- same method name and parameter list
- compatible return type (same or covariant)
- access level cannot be more restrictive
- use
@Overrideto catch mistakes at compile time
Dynamic dispatch (runtime polymorphism)
Dynamic dispatch means Java chooses the overridden method at runtime based on the real object type, not the variable type.
Animal a1 = new Dog("Rex");
Animal a2 = new Cat("Misty");
a1.speak(); // Dog version
a2.speak(); // Cat version
Even though both variables are typed as Animal, the JVM calls the correct subclass implementation.
This is the practical core of polymorphism.
Why polymorphism is powerful
Polymorphism lets you write code that depends on contracts (parent type/interface), not implementations.
List<Animal> animals = List.of(new Dog("Rex"), new Cat("Misty"));
for (Animal animal : animals) {
animal.speak(); // one loop, many behaviors
}
You can add new animal types without rewriting this loop.
Common mistakes to avoid
- forcing inheritance when composition would be clearer
- forgetting
@Override - exposing too much parent internals to children
- deep inheritance trees that are hard to reason about
Inheritance vs composition (quick rule)
Think in terms of relationship type:
- Inheritance (
is-a):Dog is an Animal - Composition (
has-a):Car has an Engine
When to prefer inheritance
Use inheritance when the child can truly be used anywhere the parent is expected, without surprising behavior. This usually means shared, stable domain behavior and a clear subtype relation.
When to prefer composition
Use composition when one object needs another object to do part of its job. Instead of inheriting behavior, you inject or hold a collaborator.
Bad inheritance example:
class Engine {
void start() {}
}
// Wrong domain relation: a Car is not an Engine
class Car extends Engine {}
Better composition example:
class Engine {
void start() {
System.out.println("Engine started");
}
}
class Car {
private final Engine engine;
Car(Engine engine) {
this.engine = engine;
}
void start() {
engine.start();
}
}
Composition is often easier to evolve and test:
- swap implementations without changing class hierarchy
- reduce tight coupling
- avoid deep inheritance trees
Mini example with context
abstract class Notification {
abstract void send(String message);
}
class EmailNotification extends Notification {
@Override
void send(String message) {
System.out.println("Email: " + message);
}
}
class SmsNotification extends Notification {
@Override
void send(String message) {
System.out.println("SMS: " + message);
}
}
Client code can treat all notifications uniformly, while runtime dispatch selects the concrete behavior.
Takeaway
For this step, remember:
extendsmodels inheritancesupergives access to parent constructor/behavior- overriding customizes behavior in subclasses
- dynamic dispatch makes polymorphism work at runtime
Mastering these concepts prepares you for interfaces, abstraction, and cleaner architecture design.