Polymorphismus
Polymorphismus in Java verstehen: Laufzeit-Polymorphismus, Compile-Zeit-Polymorphismus, Upcasting, Downcasting und praktische Anwendungen.
Polymorphismus in Java
Polymorphismus bedeutet woertlich “Vielgestaltigkeit”. In Java heisst das: Ein Objekt kann als sein eigener Typ oder als ein Typ seiner Oberklasse/seines Interfaces behandelt werden — und trotzdem das richtige Verhalten zeigen.
Was ist Polymorphismus?
Stell dir vor, du hast verschiedene Tiere und rufst bei jedem laut() auf:
public class Tier {
String name;
Tier(String name) {
this.name = name;
}
String laut() {
return "...";
}
}
public class Hund extends Tier {
Hund(String name) { super(name); }
@Override
String laut() { return "Wuff!"; }
}
public class Katze extends Tier {
Katze(String name) { super(name); }
@Override
String laut() { return "Miau!"; }
}
public class Vogel extends Tier {
Vogel(String name) { super(name); }
@Override
String laut() { return "Piep!"; }
}
Jetzt kommt die Magie:
// Alle Tiere in einer Liste (Typ: Tier)
Tier[] tiere = {
new Hund("Rex"),
new Katze("Mia"),
new Vogel("Tweety"),
new Hund("Bella")
};
// Polymorphismus: Java ruft die RICHTIGE Methode auf!
for (var tier : tiere) {
System.out.printf("%s sagt: %s%n", tier.name, tier.laut());
}
Ausgabe:
Rex sagt: Wuff!
Mia sagt: Miau!
Tweety sagt: Piep!
Bella sagt: Wuff!
Obwohl die Variable den Typ Tier hat, ruft Java die ueberschriebene Methode der tatsaechlichen Klasse auf. Das ist Polymorphismus!
Arten von Polymorphismus
1. Laufzeit-Polymorphismus (Methoden ueberschreiben)
Die Methode wird zur Laufzeit basierend auf dem tatsaechlichen Objekttyp ausgewaehlt:
Tier tier = new Hund("Rex"); // Variable: Tier, Objekt: Hund
tier.laut(); // Ruft Hund.laut() auf --> "Wuff!"
2. Compile-Zeit-Polymorphismus (Methoden ueberladen)
Die Methode wird zur Kompilierzeit basierend auf den Argumenten ausgewaehlt:
static void ausgabe(int x) { System.out.println("int: " + x); }
static void ausgabe(double x) { System.out.println("double: " + x); }
static void ausgabe(String x) { System.out.println("String: " + x); }
ausgabe(42); // int: 42
ausgabe(3.14); // double: 3.14
ausgabe("Hallo"); // String: Hallo
Upcasting und Downcasting
Upcasting (automatisch)
Ein Objekt einer Unterklasse kann als Oberklasse behandelt werden:
Hund rex = new Hund("Rex");
Tier tier = rex; // Upcasting: Hund -> Tier (automatisch)
tier.laut(); // "Wuff!" -- richtige Methode!
// tier.bellen(); // FEHLER! Tier hat kein bellen()
Downcasting (explizit)
Zurueckcasten braucht einen expliziten Cast:
Tier tier = new Hund("Rex");
// Pruefung mit instanceof
if (tier instanceof Hund hund) {
hund.bellen(); // Jetzt koennen wir Hund-Methoden nutzen
}
// Pattern Matching (Java 16+)
Tier tier = new Katze("Mia");
if (tier instanceof Katze katze) {
katze.miauen();
} else if (tier instanceof Hund hund) {
hund.bellen();
}
Polymorphismus mit Interfaces
Interfaces sind der haeufigste Anwendungsfall fuer Polymorphismus:
interface Zeichenbar {
void zeichnen();
}
class Kreis implements Zeichenbar {
@Override
public void zeichnen() {
System.out.println("Zeichne einen Kreis");
}
}
class Rechteck implements Zeichenbar {
@Override
public void zeichnen() {
System.out.println("Zeichne ein Rechteck");
}
}
class Dreieck implements Zeichenbar {
@Override
public void zeichnen() {
System.out.println("Zeichne ein Dreieck");
}
}
// Alle Formen in einer Liste
var formen = List.<Zeichenbar>of(
new Kreis(),
new Rechteck(),
new Dreieck(),
new Kreis()
);
// Polymorphismus: Jede Form zeichnet sich selbst
for (var form : formen) {
form.zeichnen();
}
Praktische Beispiele
Zahlungssystem
interface Zahlungsmethode {
boolean bezahlen(double betrag);
String getName();
}
class Bargeld implements Zahlungsmethode {
private double vorrat;
Bargeld(double vorrat) { this.vorrat = vorrat; }
@Override
public boolean bezahlen(double betrag) {
if (betrag <= vorrat) {
vorrat -= betrag;
System.out.printf("%.2f EUR bar bezahlt. Rest: %.2f EUR%n", betrag, vorrat);
return true;
}
System.out.println("Nicht genug Bargeld!");
return false;
}
@Override
public String getName() { return "Bargeld"; }
}
class Kreditkarte implements Zahlungsmethode {
private String nummer;
private double limit;
private double ausgegeben;
Kreditkarte(String nummer, double limit) {
this.nummer = nummer;
this.limit = limit;
}
@Override
public boolean bezahlen(double betrag) {
if (ausgegeben + betrag <= limit) {
ausgegeben += betrag;
System.out.printf("%.2f EUR mit Karte %s bezahlt.%n", betrag, nummer);
return true;
}
System.out.println("Kreditlimit erreicht!");
return false;
}
@Override
public String getName() { return "Kreditkarte " + nummer; }
}
// Polymorphismus: Gleicher Code fuer verschiedene Zahlungsmethoden
Zahlungsmethode methode = new Kreditkarte("****1234", 5000);
methode.bezahlen(49.99); // Funktioniert mit jeder Zahlungsmethode!
// Auch in Listen:
var methoden = List.<Zahlungsmethode>of(
new Bargeld(100),
new Kreditkarte("****5678", 2000)
);
for (var m : methoden) {
System.out.println("Bezahle mit: " + m.getName());
m.bezahlen(25.50);
}
Benachrichtigungssystem
interface Benachrichtigung {
void senden(String empfaenger, String nachricht);
}
class EmailBenachrichtigung implements Benachrichtigung {
@Override
public void senden(String empfaenger, String nachricht) {
System.out.printf("E-Mail an %s: %s%n", empfaenger, nachricht);
}
}
class SMSBenachrichtigung implements Benachrichtigung {
@Override
public void senden(String empfaenger, String nachricht) {
System.out.printf("SMS an %s: %s%n", empfaenger, nachricht);
}
}
class PushBenachrichtigung implements Benachrichtigung {
@Override
public void senden(String empfaenger, String nachricht) {
System.out.printf("Push an %s: %s%n", empfaenger, nachricht);
}
}
// Die Methode arbeitet mit dem Interface -- egal welche Implementierung
static void benachrichtigeAlle(List<Benachrichtigung> kanaele,
String empfaenger, String nachricht) {
for (var kanal : kanaele) {
kanal.senden(empfaenger, nachricht);
}
}
var kanaele = List.<Benachrichtigung>of(
new EmailBenachrichtigung(),
new SMSBenachrichtigung(),
new PushBenachrichtigung()
);
benachrichtigeAlle(kanaele, "max@example.com", "Deine Bestellung ist da!");
Pattern Matching mit switch (Java 21+)
static String beschreibe(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "Positive Ganzzahl: " + i;
case Integer i -> "Nicht-positive Ganzzahl: " + i;
case String s when s.length() > 10 -> "Langer Text";
case String s -> "Kurzer Text: " + s;
case Double d -> "Dezimalzahl: " + d;
case null -> "null-Wert";
default -> "Unbekannter Typ: " + obj.getClass().getSimpleName();
};
}
Uebungen
Uebung 1: Tier-Polymorphismus
Erstelle eine Hierarchie mit verschiedenen Tieren und einer Methode tierGeraeuscheAbspielen(List<Tier> tiere), die alle Tiere ihre Laute machen laesst.
Uebung 2: Formberechnung
Erstelle ein Interface Form mit flaeche() und umfang(). Implementiere es fuer Kreis, Rechteck und Dreieck. Schreibe eine Methode, die die Gesamtflaeche einer Liste von Formen berechnet.
Uebung 3: Sortierbares Interface
Erstelle ein Interface Sortierbar mit vergleicheMit(Sortierbar anderes). Implementiere es fuer Student (nach Note) und nutze Polymorphismus zum Sortieren.
Uebung 4: Plugin-Manager
Erstelle ein Interface Plugin und mehrere Implementierungen. Schreibe einen PluginManager, der alle Plugins laden und ausfuehren kann.
Was kommt als Naechstes?
In der naechsten Lektion lernst du Kapselung (Encapsulation) — wie du mit Getter und Setter den Zugriff auf Felder kontrollierst und Daten schuetzt.
Zusammenfassung
- Polymorphismus bedeutet: Ein Objekt verhaelt sich je nach seinem tatsaechlichen Typ
- Laufzeit-Polymorphismus: Ueberschriebene Methoden werden zur Laufzeit aufgeloest
- Compile-Zeit-Polymorphismus: Ueberladene Methoden werden beim Kompilieren aufgeloest
- Upcasting (Unterklasse -> Oberklasse) passiert automatisch
- Downcasting (Oberklasse -> Unterklasse) braucht
instanceof-Pruefung - Polymorphismus mit Interfaces ist der haeufigste und flexibelste Ansatz
- Pattern Matching in switch macht Polymorphismus noch eleganter
Pro-Tipp: Polymorphismus ist der Schlussel zu flexiblem, erweiterbarem Code. Wenn du merkst, dass du lange if-else-Ketten oder switch-Statements schreibst, die basierend auf einem Typ unterschiedliches Verhalten ausfuehren, ist das oft ein Zeichen, dass du Polymorphismus verwenden solltest. Ersetze die Bedingungen durch eine gemeinsame Schnittstelle und verschiedene Implementierungen!