Skip to content
IRC-CodingIRC-Coding
OOPInheritancePolymorphismLiskov Substitution PrincipleCompositionOverrideInterfaceJava

OOP Inheritance & Polymorphism Basics

Master OOP inheritance, polymorphism, Liskov Substitution Principle, and composition with Java examples.

S

schutzgeist

13 min read
OOP Inheritance & Polymorphism Basics

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

  1. Overriding vs Overloading

    • Overriding: Same signature in subclass
    • Overloading: Same name, different parameters
  2. Abstract Class vs Interface

    • Abstract class: Shared implementation
    • Interface: Pure contract
  3. Is-a vs Has-a

    • Is-a: Inheritance
    • Has-a: Composition
  4. 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.

Back to Blog
Share:

Nächster Artikel in Object-Oriented Programming

Weiterlesen
OOP Basics: Classes, Objects, Methods & Inheritance

Related Posts