Skip to content
IRC-Coding IRC-Coding
OOP Vererbung Inheritance Polymorphie Liskov Komposition Override Interface

OOP Vererbung Grundlagen: Inheritance & Polymorphie

OOP Vererbung mit Inheritance, Polymorphie, Liskov Substitution und Komposition. Java Beispiele für ist-ein Beziehungen.

S

schutzgeist

2 min read

OOP Vererbung Grundlagen: Inheritance & Polymorphie

Vererbung (Inheritance) ist ein zentrales Prinzip der objektorientierten Programmierung, das es ermöglicht, gemeinsame Eigenschaften und Verhalten in einer Basisklasse zu definieren und in Unterklassen wiederzuverwenden.

Was ist Vererbung?

Vererbung bildet eine “ist-ein” Beziehung zwischen Typen, bei der eine Unterklasse alle öffentlich und geschützten Merkmale der Basisklasse erbt und diese erweitern oder überschreiben kann.

Kernkonzepte der Vererbung

  • ist-ein Beziehung: Unterklasse ist eine Spezialisierung der Basisklasse
  • Code Wiederverwendung: Gemeinsames Verhalten wird zentral definiert
  • Polymorphie: Objekte können als Basistyp behandelt werden
  • Erweiterbarkeit: Neue Funktionalität kann hinzugefügt werden

Grundlegende Vererbung in Java

Einfache Vererbungshierarchie

// 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 + "}";
    }
}

Polymorphe Nutzung

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());
            }
        }
    }
}

Vererbung in C#

C# Vererbung mit 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 Vererbung

from abc import ABC, abstractmethod
from typing import List

# Abstrakte Basisklasse
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}}}"

# Konkrete Unterklasse
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}}}"

# Weitere Unterklasse
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}}}"

# Polymorphe Nutzung
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 mit 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}")

# Verwendung
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)

Prinzip verstehen

Das Liskov Substitution Principle besagt, dass Unterklassen ihre Basisklassen ersetzen können müssen, ohne dass das Programmverhalten unerwartet ändert.

LSP-Verletzung Beispiel

// Schlechtes Design - Verletzt 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;
    }
}

// Problematische Unterklasse
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-Verletzung in der Praxis
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-Konformes Design

// Besser: Abstrakte Basisklasse
public abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

// Rectangle als eigenständige Klasse
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 als eigenständige Klasse
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; }
}

Komposition vor Vererbung

Problem der starren Hierarchien

// Schlecht: Tiefe Vererbungshierarchie
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 ist kein echtes Tier!
    @Override
    public void eat() { 
        throw new UnsupportedOperationException("Robots don't eat"); 
    }
}

Besser: Komposition

// Interfaces für Verhalten
public interface Eater {
    void eat();
}

public interface Walker {
    void walk();
}

public interface Barker {
    void bark();
}

// Basisklasse mit grundlegendem Verhalten
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");
    }
}

// Komposition für Verhaltensweisen
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 an Animal
    public void eat() {
        animal.eat();
    }
}

// Flexibler für 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");
    }
}

Abstrakte Klassen vs Interfaces

Abstrakte Klasse

// 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

Mehrfachvererbung 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 gelöst mit virtual inheritance
class Bat : public Mammal, public Bird {
public:
    void echolocate() { std::cout << "Bat echolocating" << std::endl; }
};

int main() {
    Bat bat;
    bat.eat();        // Eindeutig durch virtual inheritance
    bat.walk();       // Von Mammal
    bat.fly();        // Von Bird
    bat.echolocate(); // Eigene Methode
    return 0;
}

Interface-basierte Lösung in Java/C#

// Interface für Fähigkeiten
public interface Flyable {
    void fly();
}

public interface Walkable {
    void walk();
}

// Basisklasse
public abstract class Animal {
    public abstract void eat();
}

// Klasse implementiert mehrere 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 für Vererbung

1. Tiefe Hierarchien vermeiden

// Schlecht: Zu tief
class Animal -> Mammal -> Dog -> Labrador -> GoldenRetriever

// Besser: Flacher
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever

2. Final für stabile Klassen

public final class ImmutablePoint {
    private final double x, y;
    
    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    // Kann nicht beerbt werden - garantiert Stabilität
}

3. Template Method Pattern

public abstract class DataProcessor {
    
    // Template Method - definiert Ablauf
    public final void processData() {
        loadData();
        validateData();
        transformData();
        saveData();
        cleanup();
    }
    
    // Konkrete Methoden
    private void loadData() {
        System.out.println("Loading data...");
    }
    
    private void cleanup() {
        System.out.println("Cleaning up...");
    }
    
    // Abstrakte Methoden - werden von Unterklassen implementiert
    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");
    }
}

Prüfungsrelevante Konzepte

Wichtige Unterscheidungen

  1. Überschreiben vs Überladen

    • Überschreiben: Gleiche Signatur in Unterklasse
    • Überladen: Gleicher Name, unterschiedliche Parameter
  2. Abstrakte Klasse vs Interface

    • Abstrakte Klasse: Gemeinsame Implementierung
    • Interface: Reiner Vertrag
  3. ist-ein vs hat-ein

    • ist-ein: Vererbung
    • hat-ein: Komposition
  4. Kovarianz vs Kontravarianz

    • Kovarianz: Rückgabetypen können spezifischer sein
    • Kontravarianz: Parametertypen können allgemeiner sein

Typische Prüfungsaufgaben

// Polymorphie Beispiel
public class PolymorphismDemo {
    public static void main(String[] args) {
        Shape[] shapes = {
            new Rectangle("rot", 0, 0, 5, 3),
            new Circle("blau", 10, 10, 2)
        };
        
        for (Shape shape : shapes) {
            // Dynamischer Dispatch
            System.out.println(shape.area());
            
            // Type Casting
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                System.out.println("Width: " + rect.getWidth());
            }
        }
    }
}

Zusammenfassung

Vererbung ist ein mächtiges Werkzeug, aber erfordert sorgfältige Anwendung:

  • Nutze Vererbung für echte ist-ein Beziehungen
  • Beachte das Liskov Substitution Principle
  • Bevorzuge Komposition bei reinem Code-Reuse
  • Halte Hierarchien flach und stabil
  • Nutze Interfaces für flexible Verträge
  • Verwende abstrakte Klassen für gemeinsame Implementierung

Gute Vererbung fördert Wiederverwendung und Polymorphie, während schlechte Vererbung zu engen Kopplungen und fragilen Architekturen führt.

Zurück zum Blog
Share:

Ähnliche Beiträge