Polymorphie (Polymorphism)
In dieser Lerneinheit entdeckst du das Konzept der Polymorphie, das es ermöglicht, Methoden gleichen Namens in verschiedenen Klassen unterschiedlich zu implementieren. Du verstehst, wie Polymorphie die Flexibilität und Wartbarkeit deines Codes erhöht und lernst, wie du dieses Prinzip in der Praxis für die Entwicklung robuster Softwaresysteme einsetzt. Durch praktische Beispiele erfährst du, wie Polymorphie die Erweiterbarkeit deiner Anwendungen verbessert und Code-Redundanz reduziert.
Einführung
Stell dir vor, du entwickelst eine Anwendung mit verschiedenen Fahrzeugtypen. Jedes Fahrzeug soll eine fahre() Methode haben, aber die Implementierung ist unterschiedlich.
Wie kannst du mit einer einheitlichen Schnittstelle arbeiten, obwohl die Fahrzeuge sich unterschiedlich verhalten?

Polymorphie (Polymorphism) ermöglicht genau das: Du kannst Methoden mit gleichem Namen in verschiedenen Klassen unterschiedlich implementieren. Der Code wird flexibel und erweiterbar.
In dieser Lerneinheit lernst du, wie Polymorphie funktioniert, welche Arten es gibt (Overloading und Overriding) und wie dynamische Bindung zur Laufzeit entscheidet, welche Methode aufgerufen wird.
Lernziele
Nach dieser Lerneinheit kannst du:
- Polymorphismus als Konzept erklären und die zwei Hauptarten unterscheiden
- Überladen (Overloading) verstehen und anwenden
- Dynamische Bindung (Late Binding) und ihre Bedeutung erklären
- Abstrakte Klassen und ihre Rolle in der Polymorphie verstehen
- Interfaces (Schnittstellen) gezielt einsetzen
Was ist Polymorphismus?
Polymorphismus bedeutet “Vielgestaltigkeit”. Es erlaubt dir, Methoden mit dem gleichen Namen in verschiedenen Klassen unterschiedlich zu implementieren.
Warum brauchst du Polymorphie? Du schreibst flexiblen Code mit einheitlicher Schnittstelle, obwohl die Implementierungen unterschiedlich sind.
Es gibt zwei Hauptarten von Polymorphismus:
-
Überladen (Overloading): Mehrere Methoden mit gleichem Namen in einer Klasse, aber unterschiedlichen Parametern. Entscheidung zur Kompilierzeit.
-
Überschreiben (Overriding): Eine Unterklasse implementiert eine Methode der Basisklasse neu. Entscheidung zur Laufzeit (dynamische Bindung).
Überladen (Overloading) von Methoden
Überladen ermöglicht es dir, innerhalb derselben Klasse mehrere Methoden mit dem gleichen Namen zu haben, aber mit unterschiedlichen Parametertypen oder -anzahlen.
Die Entscheidung, welche Methode aufgerufen wird, trifft der Compiler basierend auf den übergebenen Parametern. Deshalb wird Overloading auch Kompilierzeit-Polymorphismus oder statischer Polymorphismus genannt.
Vorteil: Du arbeitest mit verschiedenen Datentypen unter einem einheitlichen Methodennamen. Der Code wird flexibel und intuitiv.
Overloading einfach erklärt
Stell dir vor, du hast eine Fernbedienung mit einem “An/Aus”-Knopf. Dieser Knopf funktioniert für verschiedene Geräte: TV, Radio, Lampe.
Der Name ist gleich (“An/Aus”), aber die Aktion ist unterschiedlich je nachdem, welches Gerät du steuerst.
Genauso funktioniert Overloading: Du hast mehrere Methoden mit dem gleichen Namen, aber sie erwarten unterschiedliche Parameter.
Der Compiler schaut auf die Parameter und entscheidet automatisch, welche Version der Methode er aufrufen soll. Du musst dir keine Gedanken machen - es passiert automatisch zur Kompilierzeit.
Overloading: Ein einfaches Beispiel
Schauen wir uns eine Klasse Demo an, die die gleiche Methode show() für verschiedene Datentypen anbietet:
class Demo {
void show(int a) {
System.out.println("Integer: " + a);
}
void show(String b) {
System.out.println("String: " + b);
}
}Die erste Version nimmt einen int, die zweite einen String Parameter. Der Compiler entscheidet basierend auf dem übergebenen Typ, welche Methode aufgerufen wird.
Anwendung:
Demo obj = new Demo();
obj.show(5); // Ruft show(int a) auf
obj.show("Hallo"); // Ruft show(String b) aufOverloading in der Praxis: ShapeDrawer
Ein praktisches Beispiel aus der Grafikprogrammierung. Die Klasse ShapeDrawer nutzt Overloading, um verschiedene Formen zu zeichnen:
class ShapeDrawer {
void draw(Circle circle) {
// Zeichnet einen Kreis
}
void draw(Square square) {
// Zeichnet ein Quadrat
}
void draw(Circle circle, String color) {
// Zeichnet einen Kreis mit Farbe
}
}Die draw() Methode verzweigt sich automatisch je nach Parametern: Kreis, Quadrat oder Kreis mit Farbe. Der Code bleibt übersichtlich und die API intuitiv.
Überschreiben einfach erklärt
Stell dir vor, deine Elternklasse sagt: “Alle Fahrzeuge machen Geräusche”.
Deine Kinderklasse Auto sagt aber: “Moment, ich mache ein spezielles Geräusch - ich hupe!”
Das ist Überschreiben (Overriding): Die Kindklasse nimmt eine Methode der Elternklasse und implementiert sie neu, auf ihre eigene Art.
Der Unterschied zu Overloading: Bei Overloading sind die Parameter unterschiedlich. Bei Overriding ist alles gleich (Name, Parameter), aber die Implementierung ist anders.
Wichtig: Die Entscheidung, welche Methode aufgerufen wird, erfolgt zur Laufzeit basierend auf dem tatsächlichen Objekttyp.
Überschreiben (Overriding) - Kurzer Rückblick
Wie du in der vorherigen Lerneinheit (Vererbung) gelernt hast, erlaubt Overriding einer Unterklasse, eine Methode der Basisklasse neu zu implementieren.
Beim Polymorphismus nutzen wir diese Fähigkeit, um flexiblen Code zu schreiben:
Fahrzeug meinFahrzeug = new Auto();
meinFahrzeug.fahre(); // Ruft Auto.fahre() aufDie Referenzvariable hat den Typ Fahrzeug, aber das Objekt ist ein Auto. Zur Laufzeit wird die Methode fahre() der Klasse Auto aufgerufen, nicht die von Fahrzeug.
Das ist dynamische Bindung und das Herzstück von Laufzeit-Polymorphismus.
Warum ist Polymorphismus wichtig?
Polymorphismus macht deinen Code flexibel und erweiterbar:
Wiederverwendbarkeit: Du schreibst generischen Code, der mit unterschiedlichen Datentypen arbeiten kann. Eine Methode starteReise(Fahrzeug f) funktioniert mit Auto, Fahrrad und Motorrad.
Erweiterbarkeit: Neue Klassen können hinzugefügt werden, ohne bestehenden Code zu ändern. Füge Motorrad hinzu, und starteReise() funktioniert sofort.
Flexible APIs: Schnittstellen und abstrakte Klassen definieren Verträge, die durch verschiedene Implementierungen ausgefüllt werden können.
Dynamische Bindung (Late Binding)
Bei Overriding wird die Methode zur Laufzeit ausgewählt, nicht zur Kompilierzeit. Das nennt man dynamische Bindung oder Late Binding.
Was passiert? Der Typ der Referenzvariable (z.B. Fahrzeug) ist nicht entscheidend. Entscheidend ist der tatsächliche Objekttyp (z.B. Auto).
Fahrzeug f1 = new Auto();
Fahrzeug f2 = new Fahrrad();
f1.fahre(); // Ruft Auto.fahre() auf
f2.fahre(); // Ruft Fahrrad.fahre() aufZur Laufzeit schaut die Java Virtual Machine (JVM), welcher Objekttyp tatsächlich vorliegt und ruft die entsprechende Methode auf.
Dynamische Bindung: Praxisbeispiel
Stell dir eine Methode starteReise() vor, die ein Fahrzeug als Parameter nimmt:
void starteReise(Fahrzeug f) {
f.fahre();
}Du kannst jedes Fahrzeug übergeben:
starteReise(new Auto()); // Ruft Auto.fahre() auf
starteReise(new Fahrrad()); // Ruft Fahrrad.fahre() auf
starteReise(new Motorrad()); // Ruft Motorrad.fahre() aufDer Vorteil: Die Methode starteReise() muss nicht geändert werden, wenn du neue Fahrzeugtypen hinzufügst. Das ist das Open/Closed Principle: Offen für Erweiterung, geschlossen für Modifikation.
Abstrakte Klassen
Wie du in der vorherigen Lerneinheit (Vererbung) gehört hast, dienen abstrakte Klassen als Blaupausen für andere Klassen. Sie können nicht direkt instanziiert werden.
Eine abstrakte Klasse kombiniert zwei Arten von Methoden:
Abstrakte Methoden: Haben keine Implementierung. Sie dienen als Platzhalter, die in abgeleiteten Klassen überschrieben werden müssen.
Implementierte Methoden: Bieten gemeinsame Funktionalität, die in abgeleiteten Klassen direkt genutzt oder überschrieben werden kann.
Hauptzweck: Code-Wiederverwendung durch gemeinsame Struktur in der Basis-Klasse.
Abstrakte Klassen: Code-Beispiel
Eine abstrakte Klasse Fahrzeug mit abstrakter und implementierter Methode:
abstract class Fahrzeug {
private int geschwindigkeit;
// Implementierte Methode (gemeinsam für alle)
public void beschleunigen(int wert) {
geschwindigkeit += wert;
}
// Abstrakte Methode (muss überschrieben werden)
public abstract void anhalten();
}Abgeleitete Klassen müssen anhalten() implementieren:
class Auto extends Fahrzeug {
public void anhalten() {
System.out.println("Auto bremst mit ABS");
}
}Wichtig: Fahrzeug kann nicht instanziiert werden: new Fahrzeug() führt zu einem Compiler-Fehler.
Schnittstellen (Interfaces)
Eine Schnittstelle (Interface) definiert ein Vertragsmodell: Sie legt fest, welche Methoden eine Klasse implementieren muss, aber nicht wie.
Interfaces enthalten nur Methodensignaturen (in neueren Java-Versionen können auch Default-Implementierungen vorhanden sein).
Hauptvorteile:
Mehrfachvererbung: Eine Klasse kann mehrere Interfaces implementieren. Das ist in Sprachen wie Java wichtig, die keine Mehrfachvererbung von Klassen erlauben.
Flexibilität: Verschiedene, nicht zusammenhängende Klassen können dasselbe Interface implementieren und damit austauschbar werden.
Interfaces: Code-Beispiel
Ein Interface Bewegbar definiert den Vertrag:
interface Bewegbar {
void bewege(int x, int y);
}Verschiedene Klassen können dieses Interface implementieren:
class Roboter implements Bewegbar {
public void bewege(int x, int y) {
System.out.println("Roboter bewegt sich zu (" + x + ", " + y + ")");
}
}
class Drohne implements Bewegbar {
public void bewege(int x, int y) {
System.out.println("Drohne fliegt zu (" + x + ", " + y + ")");
}
}Polymorphismus mit Interfaces:
Bewegbar objekt = new Roboter();
objekt.bewege(10, 20); // Ruft Roboter.bewege() aufAbstrakte Klassen vs. Interfaces - Wann was?
Verwende abstrakte Klassen wenn:
- Mehrere Klassen eine gemeinsame Struktur oder Implementierung teilen
- Du Zustand (Attribute) oder implementierte Methoden teilen möchtest
- Die Klassen eng verwandt sind (“ist-ein”-Beziehung)
Verwende Interfaces wenn:
- Du Verhalten definierst, das in verschiedenen, nicht zusammenhängenden Klassen implementiert werden kann
- Du Mehrfachvererbung benötigst (eine Klasse kann mehrere Interfaces implementieren)
- Du maximale Flexibilität wünschst: Verschiedene Implementierungen können leicht ausgetauscht werden
Faustregel: Abstrakte Klassen für gemeinsame Basis, Interfaces für gemeinsames Verhalten.
Zusammenfassung und Ausblick
Zusammenfassung
Polymorphismus bedeutet, dass Methoden mit gleichem Namen unterschiedlich implementiert werden können. Das macht Code flexibel und erweiterbar.
Die zwei Formen
Überladen (Overloading) - Kompilierzeit-Polymorphismus:
- Mehrere Methoden mit gleichem Namen in einer Klasse
- Unterscheiden sich durch Parameter (Typ, Anzahl, Reihenfolge)
- Compiler wählt passende Version zur Kompilierzeit
- Beispiel:
show(int a)undshow(String b)
Überschreiben (Overriding) - Laufzeit-Polymorphismus:
- Unterklasse implementiert Methode der Basisklasse neu
- Gleicher Name, gleiche Parameter, neue Implementierung
- Entscheidung zur Laufzeit durch dynamische Bindung
- Beispiel:
AutoüberschreibtFahrzeug.fahre()
Dynamische Bindung: Bei
Fahrzeug f = new Auto()ruftf.fahre()die Methode vonAutoauf, nicht vonFahrzeug- entscheidend ist der tatsächliche Objekttyp zur Laufzeit.
Abstrakte Klassen vs. Interfaces
Abstrakte Klassen: Kombinieren abstrakte Methoden (Platzhalter) mit fertigen Implementierungen. Können nicht instanziiert werden. Nutze sie für gemeinsame Basis mit teilweiser Implementierung.
Interfaces: Reine Verträge ohne Implementierung. Eine Klasse kann mehrere implementieren. Nutze sie für gemeinsames Verhalten unabhängiger Klassen.
Warum Polymorphie? Du schreibst Code gegen Schnittstellen statt konkrete Klassen. Neue Typen können hinzugefügt werden, ohne bestehenden Code zu ändern (Open/Closed Principle).
Ausblick
In der nächsten Lerneinheit wiederholst du alle OOP-Grundlagen und erhältst einen Ausblick auf weiterführende Themen wie Design Patterns.