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
-
Überschreiben vs Überladen
- Überschreiben: Gleiche Signatur in Unterklasse
- Überladen: Gleicher Name, unterschiedliche Parameter
-
Abstrakte Klasse vs Interface
- Abstrakte Klasse: Gemeinsame Implementierung
- Interface: Reiner Vertrag
-
ist-ein vs hat-ein
- ist-ein: Vererbung
- hat-ein: Komposition
-
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.