Skip to content
IRC-Coding IRC-Coding
OOP Inheritance Polymorphism Liskov Substitution Principle Composition Override Interface Java

OOP Inheritance Basics: Polymorphism & Composition

Master OOP inheritance with polymorphism, Liskov Substitution, and composition. Java examples for is-a relationships.

S

schutzgeist

2 min read

OOP Inheritance Basics: Inheritance & Polymorphism

Inheritance is a central principle of object-oriented programming that makes it possible to define common properties and behavior in a base class and reuse them in subclasses.

What is Inheritance?

Inheritance forms an “is-a” relationship between types, in which 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: Subclass is a specialization of the base class
  • Code reuse: Common behavior is defined centrally
  • Polymorphism: Objects can be treated as base type
  • Extensibility: New functionality can be added

Basic Inheritance in Java

Simple Inheritance Hierarchy

// Abstrakte Basisklasse
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;
    }
    
    // Abstrakte Methode - muss implementiert werden
    public abstract double area();
    public abstract double perimeter();
    
    // Konkrete Methode - kann überschrieben werden
    public void move(double dx, double dy) {
        this.x += dx;
        this.y += dy;
        System.out.println("Form verschoben nach (" + x + ", " + y + ")");
    }
    
    // Getter
    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 + "}";
    }
}

// Konkrete Unterklasse
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); // Aufruf des Basisklassen-Konstruktors
        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); // Basismethode aufrufen
        System.out.println("Rechteck bewegt");
    }
    
    // Zusätzliche Methoden
    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;
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    
    @Override
    public String toString() {
        return "Rectangle{" + super.toString() + 
               ", width=" + width + ", height=" + height + "}";
    }
}

// Weitere Unterklasse
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 muss positiv sein");
        }
        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) {
        // Polymorphe Referenzen
        List<Shape> shapes = new ArrayList<>();
        
        shapes.add(new Rectangle("rot", 0, 0, 5, 3));
        shapes.add(new Circle("blau", 10, 10, 2));
        shapes.add(new Rectangle("grün", 5, 5, 2, 2));
        
        // Polymorphe Verarbeitung
        double totalArea = 0;
        for (Shape shape : shapes) {
            System.out.println(shape);
            System.out.println("Fläche: " + shape.area());
            System.out.println("Umfang: " + shape.perimeter());
            totalArea += shape.area();
            System.out.println("---");
        }
        
        System.out.println("Gesamtfläche: " + totalArea);
        
        // Dynamische Methodenaufrufe
        processShapes(shapes);
    }
    
    public static void processShapes(List<Shape> shapes) {
        for (Shape shape : shapes) {
            // Dynamischer Dispatch - je nach Objekttyp
            shape.move(1, 1);
            
            // Type Checking und Casting
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                System.out.println("Rechteck: " + rect.getWidth() + "x" + rect.getHeight());
            } else if (shape instanceof Circle) {
                Circle circle = (Circle) shape;
                System.out.println("Kreis mit Radius: " + circle.getRadius());
            }
        }
    }
}

Inheritance in C#

C# Inheritance with Interfaces

// Interface für gemeinsamen Vertrag
public interface IShape
{
    string Color { get; }
    double Area { get; }
    double Perimeter { get; }
    void Move(double dx, double dy);
}

// Abstrakte Basisklasse
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 - kann überschrieben werden
    public virtual void Move(double dx, double dy)
    {
        X += dx;
        Y += dy;
        Console.WriteLine($"Form verschoben nach ({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("Rechteck bewegt");
    }
    
    public void SetDimensions(double width, double height)
    {
        if (width <= 0 || height <= 0)
            throw new ArgumentException("Maße müssen positiv sein");
        
        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 must not be empty")
        self.color = color
        self.x = x
        self.y = y
    
    @abstractmethod
    def area(self) -> float:
        """Calculates the area of the shape"""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Calculates the perimeter of the shape"""
        pass
    
    def move(self, dx: float, dy: float):
        """Moves the shape"""
        self.x += dx
        self.y += dy
        print(f"Shape moved to ({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)  # Call base class constructor
        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)  # Call base method
        print("Rectangle moved")
    
    def set_dimensions(self, width: float, height: float):
        if width <= 0 or height <= 0:
            raise ValueError("Dimensions must be positive")
        self.width = width
        self.height = height
    
    def __str__(self):
        return f"Rectangle{{{super().__str__()}, width={self.width}, height={self.height}}}"

# Another 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 must be positive")
        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"Area: {shape.area():.2f}")
        print(f"Perimeter: {shape.perimeter():.2f}")
        total_area += shape.area()
        
        # Type checking with isinstance
        if isinstance(shape, Rectangle):
            print(f"Rectangle: {shape.width}x{shape.height}")
        elif isinstance(shape, Circle):
            print(f"Circle with radius: {shape.radius}")
        
        print("---")
    
    print(f"Total area: {total_area:.2f}")

# Usage
shapes = [
    Rectangle("red", 0, 0, 5, 3),
    Circle("blue", 10, 10, 2),
    Rectangle("green", 5, 5, 2, 2)
]

process_shapes(shapes)

Liskov Substitution Principle (LSP)

Understanding the Principle

The Liskov Substitution Principle states that subclasses must be able to replace their base classes without unexpected changes to program behavior.

LSP Violation Example

// Bad 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); // Square must have equal sides
    }
    
    @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);
    // Expectation: area() == 20
    // With Square: area() == 16 (unexpected!)
    assert rect.area() == 20 : "LSP violated!";
}

LSP-Compliant Design

// Better: Abstract base class
public abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

// Rectangle as independent 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("Dimensions must be positive");
        }
        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 independent 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("Side must be positive");
        }
        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

Problem of Rigid Hierarchies

// Bad: 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 basic 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

// Abstrakte Klasse mit gemeinsamer Implementierung
public abstract class Vehicle {
    protected String brand;
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }
    
    // Konkrete Methode
    public void startEngine() {
        System.out.println("Engine starting...");
    }
    
    // Abstrakte Methoden
    public abstract void accelerate();
    public abstract void brake();
    
    // Template Method Pattern
    public final void drive() {
        startEngine();
        accelerate();
        System.out.println("Driving...");
        brake();
    }
    
    // Getter
    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 für Verhalten
public interface Electric {
    void charge();
    int getBatteryLevel();
}

public interface Autonomous {
    void enableAutopilot();
    boolean isAutopilotActive();
}

// Klasse implementiert mehrere 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 Implementierungen
    @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;
    }
}

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 resolved 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 abilities
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
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever

2. 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 inherited - 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 - to be 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

Important Distinctions

  1. Overriding vs Overloading

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

    • Abstract Class: Common 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 Tasks

// 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 requires careful application:

  • Use inheritance for true is-a relationships
  • Follow the Liskov Substitution Principle
  • Prefer composition for mere code reuse
  • Keep hierarchies flat and stable
  • Use interfaces for flexible contracts
  • Use abstract classes for shared implementation

Good inheritance promotes reusability and polymorphism, while poor inheritance leads to tight coupling and fragile architectures.

Back to Blog
Share:

Related Posts