OOP Inheritance Fundamentals: Inheritance & Polymorphism
Inheritance is a core principle of object-oriented programming that lets you define shared properties and behavior in a base class and reuse them across subclasses.
What Is Inheritance?
Inheritance establishes an “is-a” relationship between types, where a subclass inherits all public and protected features of the base class and can extend or override them.
Core Concepts of Inheritance
- Is-a relationship: A subclass is a specialization of the base class
- Code reuse: Common behavior is defined once, centrally
- Polymorphism: Objects can be treated as their base type
- Extensibility: New functionality can be added without modifying existing code
Basic Inheritance in Java
Simple Inheritance Hierarchy
// Abstract base class
public abstract class Shape {
protected String color;
protected double x, y; // Position
public Shape(String color, double x, double y) {
this.color = Objects.requireNonNull(color);
this.x = x;
this.y = y;
}
// Abstract method - must be implemented
public abstract double area();
public abstract double perimeter();
// Concrete method - can be overridden
public void move(double dx, double dy) {
this.x += dx;
this.y += dy;
System.out.println("Shape moved to (" + x + ", " + y + ")");
}
// Getters
public String getColor() { return color; }
public double getX() { return x; }
public double getY() { return y; }
@Override
public String toString() {
return "Shape{color='" + color + "', x=" + x + ", y=" + y + "}";
}
}
// Concrete subclass
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double x, double y, double width, double height) {
super(color, x, y); // Call base class constructor
setDimensions(width, height);
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public void move(double dx, double dy) {
super.move(dx, dy); // Call base method
System.out.println("Rectangle moved");
}
// Additional methods
public void setDimensions(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive");
}
this.width = width;
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
@Override
public String toString() {
return "Rectangle{" + super.toString() +
", width=" + width + ", height=" + height + "}";
}
}
// Another subclass
public class Circle extends Shape {
private double radius;
public Circle(String color, double x, double y, double radius) {
super(color, x, y);
setRadius(radius);
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
public void setRadius(double radius) {
if (radius <= 0) {
throw new IllegalArgumentException("Radius must be positive");
}
this.radius = radius;
}
public double getRadius() { return radius; }
@Override
public String toString() {
return "Circle{" + super.toString() + ", radius=" + radius + "}";
}
}
Polymorphic Usage
public class ShapeDemo {
public static void main(String[] args) {
// Polymorphic references
List<Shape> shapes = new ArrayList<>();
shapes.add(new Rectangle("red", 0, 0, 5, 3));
shapes.add(new Circle("blue", 10, 10, 2));
shapes.add(new Rectangle("green", 5, 5, 2, 2));
// Polymorphic processing
double totalArea = 0;
for (Shape shape : shapes) {
System.out.println(shape);
System.out.println("Area: " + shape.area());
System.out.println("Perimeter: " + shape.perimeter());
totalArea += shape.area();
System.out.println("---");
}
System.out.println("Total area: " + totalArea);
// Dynamic method calls
processShapes(shapes);
}
public static void processShapes(List<Shape> shapes) {
for (Shape shape : shapes) {
// Dynamic dispatch - behavior depends on object type
shape.move(1, 1);
// Type checking and casting
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
System.out.println("Rectangle: " + rect.getWidth() + "x" + rect.getHeight());
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
System.out.println("Circle with radius: " + circle.getRadius());
}
}
}
}
Inheritance in C#
C# Inheritance with Interfaces
// Interface for shared contract
public interface IShape
{
string Color { get; }
double Area { get; }
double Perimeter { get; }
void Move(double dx, double dy);
}
// Abstract base class
public abstract class Shape : IShape
{
protected string Color { get; set; }
protected double X { get; set; }
protected double Y { get; set; }
protected Shape(string color, double x, double y)
{
Color = color ?? throw new ArgumentNullException(nameof(color));
X = x;
Y = y;
}
// Interface implementation
public string GetColor() => Color;
// Abstract properties
public abstract double Area { get; }
public abstract double Perimeter { get; }
// Virtual method - can be overridden
public virtual void Move(double dx, double dy)
{
X += dx;
Y += dy;
Console.WriteLine($"Shape moved to ({X}, {Y})");
}
public override string ToString()
{
return $"Shape{{Color='{Color}', X={X}, Y={Y}}}";
}
}
// Concrete class
public class Rectangle : Shape
{
public double Width { get; private set; }
public double Height { get; private set; }
public Rectangle(string color, double x, double y, double width, double height)
: base(color, x, y)
{
SetDimensions(width, height);
}
public override double Area => Width * Height;
public override double Perimeter => 2 * (Width + Height);
public override void Move(double dx, double dy)
{
base.Move(dx, dy);
Console.WriteLine("Rectangle moved");
}
public void SetDimensions(double width, double height)
{
if (width <= 0 || height <= 0)
throw new ArgumentException("Dimensions must be positive");
Width = width;
Height = height;
}
public override string ToString()
{
return $"Rectangle{{{base.ToString()}, Width={Width}, Height={Height}}}";
}
}
Python Inheritance
from abc import ABC, abstractmethod
from typing import List
# Abstract base class
class Shape(ABC):
def __init__(self, color: str, x: float, y: float):
if not color:
raise ValueError("Color darf nicht leer sein")
self.color = color
self.x = x
self.y = y
@abstractmethod
def area(self) -> float:
"""Berechnet die Fläche der Form"""
pass
@abstractmethod
def perimeter(self) -> float:
"""Berechnet den Umfang der Form"""
pass
def move(self, dx: float, dy: float):
"""Verschiebt die Form"""
self.x += dx
self.y += dy
print(f"Form verschoben nach ({self.x}, {self.y})")
def __str__(self):
return f"Shape{{color='{self.color}', x={self.x}, y={self.y}}}"
# Concrete subclass
class Rectangle(Shape):
def __init__(self, color: str, x: float, y: float, width: float, height: float):
super().__init__(color, x, y) # Aufruf des Basisklassen-Konstruktors
self.set_dimensions(width, height)
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def move(self, dx: float, dy: float):
super().move(dx, dy) # Basismethode aufrufen
print("Rechteck bewegt")
def set_dimensions(self, width: float, height: float):
if width <= 0 or height <= 0:
raise ValueError("Maße müssen positiv sein")
self.width = width
self.height = height
def __str__(self):
return f"Rectangle{{{super().__str__()}, width={self.width}, height={self.height}}}"
# Additional subclass
class Circle(Shape):
def __init__(self, color: str, x: float, y: float, radius: float):
super().__init__(color, x, y)
self.set_radius(radius)
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
def set_radius(self, radius: float):
if radius <= 0:
raise ValueError("Radius muss positiv sein")
self.radius = radius
def __str__(self):
return f"Circle{{{super().__str__()}, radius={self.radius}}}"
# Polymorphic usage
def process_shapes(shapes: List[Shape]):
total_area = 0
for shape in shapes:
print(shape)
print(f"Fläche: {shape.area():.2f}")
print(f"Umfang: {shape.perimeter():.2f}")
total_area += shape.area()
# Type checking with isinstance
if isinstance(shape, Rectangle):
print(f"Rechteck: {shape.width}x{shape.height}")
elif isinstance(shape, Circle):
print(f"Kreis mit Radius: {shape.radius}")
print("---")
print(f"Gesamtfläche: {total_area:.2f}")
# Usage
shapes = [
Rectangle("rot", 0, 0, 5, 3),
Circle("blau", 10, 10, 2),
Rectangle("grün", 5, 5, 2, 2)
]
process_shapes(shapes)
Liskov Substitution Principle (LSP)
Understanding the Principle
The Liskov Substitution Principle states that subclasses must be substitutable for their base classes without causing unexpected changes in program behavior.
LSP Violation Example
// Poor design — violates LSP
public class Rectangle {
protected double width, height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getWidth() { return width; }
public double getHeight() { return height; }
public double area() {
return width * height;
}
}
// Problematic subclass
public class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width); // Quadrat muss gleiche Seiten haben
}
@Override
public void setHeight(double height) {
super.setWidth(height);
super.setHeight(height);
}
}
// LSP violation in practice
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
// Erwartung: area() == 20
// Bei Square: area() == 16 (unerwartet!)
assert rect.area() == 20 : "LSP verletzt!";
}
LSP-Compliant Design
// Better: abstract base class
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
// Rectangle as its own class
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
setDimensions(width, height);
}
public void setDimensions(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Maße müssen positiv sein");
}
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
public double getWidth() { return width; }
public double getHeight() { return height; }
}
// Square as its own class
public class Square extends Shape {
private double side;
public Square(double side) {
setSide(side);
}
public void setSide(double side) {
if (side <= 0) {
throw new IllegalArgumentException("Seite muss positiv sein");
}
this.side = side;
}
@Override
public double area() {
return side * side;
}
@Override
public double perimeter() {
return 4 * side;
}
public double getSide() { return side; }
}
Composition Over Inheritance
The Problem with Deep Hierarchies
// Poor: deep inheritance hierarchy
public class Animal {
public void eat() { System.out.println("Eating"); }
}
public class Mammal extends Animal {
public void walk() { System.out.println("Walking"); }
}
public class Dog extends Mammal {
public void bark() { System.out.println("Barking"); }
}
public class RobotDog extends Dog {
// Problem: RobotDog is not a real animal!
@Override
public void eat() {
throw new UnsupportedOperationException("Robots don't eat");
}
}
Better: Composition
// Interfaces for behavior
public interface Eater {
void eat();
}
public interface Walker {
void walk();
}
public interface Barker {
void bark();
}
// Base class with core behavior
public class Animal implements Eater {
protected String name;
public Animal(String name) {
this.name = name;
}
@Override
public void eat() {
System.out.println(name + " is eating");
}
}
// Composition for behaviors
public class Dog implements Walker, Barker {
private Animal animal;
public Dog(String name) {
this.animal = new Animal(name);
}
@Override
public void walk() {
System.out.println(animal.name + " is walking");
}
@Override
public void bark() {
System.out.println(animal.name + " says: Woof!");
}
// Delegation to Animal
public void eat() {
animal.eat();
}
}
// More flexible for RobotDog
public class RobotDog implements Walker, Barker {
private String name;
private int batteryLevel;
public RobotDog(String name) {
this.name = name;
this.batteryLevel = 100;
}
@Override
public void walk() {
if (batteryLevel > 10) {
System.out.println(name + " is walking on wheels");
batteryLevel -= 5;
} else {
System.out.println(name + " needs charging");
}
}
@Override
public void bark() {
System.out.println(name + " says: Electronic Woof!");
}
public void charge() {
batteryLevel = 100;
System.out.println(name + " is fully charged");
}
}
Abstract Classes vs Interfaces
Abstract Class
// Abstract class with shared implementation
public abstract class Vehicle {
protected String brand;
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
// Concrete method
public void startEngine() {
System.out.println("Engine starting...");
}
// Abstract methods
public abstract void accelerate();
public abstract void brake();
// Template Method Pattern
public final void drive() {
startEngine();
accelerate();
System.out.println("Driving...");
brake();
}
// Getters
public String getBrand() { return brand; }
public int getYear() { return year; }
}
public class Car extends Vehicle {
public Car(String brand, int year) {
super(brand, year);
}
@Override
public void accelerate() {
System.out.println("Car accelerating");
}
@Override
public void brake() {
System.out.println("Car braking");
}
}
Interface
// Interface for behavior
public interface Electric {
void charge();
int getBatteryLevel();
}
public interface Autonomous {
void enableAutopilot();
boolean isAutopilotActive();
}
// Class implements multiple interfaces
public class Tesla extends Vehicle implements Electric, Autonomous {
private int batteryLevel = 80;
private boolean autopilotActive = false;
public Tesla(String brand, int year) {
super(brand, year);
}
@Override
public void accelerate() {
System.out.println("Tesla accelerating silently");
}
@Override
public void brake() {
System.out.println("Tesla regenerative braking");
}
// Interface implementations
@Override
public void charge() {
System.out.println("Tesla charging...");
batteryLevel = 100;
}
@Override
public int getBatteryLevel() {
return batteryLevel;
}
@Override
public void enableAutopilot() {
autopilotActive = true;
System.out.println("Autopilot enabled");
}
@Override
public boolean isAutopilotActive() {
return autopilotActive;
}
}
The Diamond Problem
Multiple Inheritance in C++
#include <iostream>
class Animal {
public:
void eat() { std::cout << "Animal eating" << std::endl; }
};
class Mammal : virtual public Animal {
public:
void walk() { std::cout << "Mammal walking" << std::endl; }
};
class Bird : virtual public Animal {
public:
void fly() { std::cout << "Bird flying" << std::endl; }
};
// Diamond Problem solved with virtual inheritance
class Bat : public Mammal, public Bird {
public:
void echolocate() { std::cout << "Bat echolocating" << std::endl; }
};
int main() {
Bat bat;
bat.eat(); // Unambiguous through virtual inheritance
bat.walk(); // From Mammal
bat.fly(); // From Bird
bat.echolocate(); // Own method
return 0;
}
Interface-Based Solution in Java/C#
// Interface for capabilities
public interface Flyable {
void fly();
}
public interface Walkable {
void walk();
}
// Base class
public abstract class Animal {
public abstract void eat();
}
// Class implements multiple interfaces
public class Bat extends Animal implements Flyable, Walkable {
@Override
public void eat() {
System.out.println("Bat eating insects");
}
@Override
public void fly() {
System.out.println("Bat flying");
}
@Override
public void walk() {
System.out.println("Bat walking");
}
public void echolocate() {
System.out.println("Bat echolocating");
}
}
Best Practices for Inheritance
1. Avoid Deep Hierarchies
// Bad: Too deep
class Animal -> Mammal -> Dog -> Labrador -> GoldenRetriever
// Better: Flatter structure
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever
2. Use Final for Stable Classes
public final class ImmutablePoint {
private final double x, y;
public ImmutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
// Cannot be subclassed - guarantees stability
}
3. Template Method Pattern
public abstract class DataProcessor {
// Template Method - defines the flow
public final void processData() {
loadData();
validateData();
transformData();
saveData();
cleanup();
}
// Concrete methods
private void loadData() {
System.out.println("Loading data...");
}
private void cleanup() {
System.out.println("Cleaning up...");
}
// Abstract methods - implemented by subclasses
protected abstract void validateData();
protected abstract void transformData();
protected abstract void saveData();
}
public class CSVProcessor extends DataProcessor {
@Override
protected void validateData() {
System.out.println("Validating CSV data");
}
@Override
protected void transformData() {
System.out.println("Transforming CSV data");
}
@Override
protected void saveData() {
System.out.println("Saving CSV data");
}
}
Exam-Relevant Concepts
Key Distinctions
-
Overriding vs Overloading
- Overriding: Same signature in subclass
- Overloading: Same name, different parameters
-
Abstract Class vs Interface
- Abstract class: Shared implementation
- Interface: Pure contract
-
Is-a vs Has-a
- Is-a: Inheritance
- Has-a: Composition
-
Covariance vs Contravariance
- Covariance: Return types can be more specific
- Contravariance: Parameter types can be more general
Typical Exam Questions
// Polymorphism example
public class PolymorphismDemo {
public static void main(String[] args) {
Shape[] shapes = {
new Rectangle("red", 0, 0, 5, 3),
new Circle("blue", 10, 10, 2)
};
for (Shape shape : shapes) {
// Dynamic dispatch
System.out.println(shape.area());
// Type casting
if (shape instanceof Rectangle) {
Rectangle rect = (Rectangle) shape;
System.out.println("Width: " + rect.getWidth());
}
}
}
}
Summary
Inheritance is a powerful tool, but it demands careful use:
- Use inheritance for genuine is-a relationships
- Follow the Liskov Substitution Principle
- Prefer composition for pure code reuse
- Keep hierarchies flat and stable
- Use interfaces for flexible contracts
- Use abstract classes for shared implementation
Good inheritance promotes reuse and polymorphism, while poor inheritance leads to tight coupling and fragile architectures.
Continuing Your OOP Learning Path
You’ve now completed all the OOP articles. Head back to the first one: Object-Oriented Programming OOP Fundamentals.

