Professionelle Java-Schulung – Vor Ort und Online.

  • Einführung in die Welt der Java-Programmierung
  • Konzepte und Techniken von Algorithmen kennenlernen
  • Datenbanken und ihren Einsatz in der digitalen Welt

Vererbung



Was ist Vererbung?

Vererbung in Java ist ein Konzept der objektorientierten Programmierung, das es ermöglicht, eine neue Klasse (Unterklasse oder abgeleitete Klasse) basierend auf einer vorhandenen Klasse (Basisklasse oder Superklasse) zu erstellen. Die Unterklasse erbt die Eigenschaften und Verhaltensweisen (Felder und Methoden) der Basisklasse und kann zusätzliche Felder und Methoden hinzufügen oder vorhandene Methoden überschreiben. Das Hauptziel der Vererbung ist es, Code-Wiederverwendung zu fördern und die Struktur und Hierarchie von Klassen in einem Programm zu organisieren. Mit Vererbung können wir allgemeine Eigenschaften und Verhaltensweisen in der Basisklasse definieren und spezifische oder zusätzliche Eigenschaften in den Unterklassen hinzufügen, die davon erben. Einige wichtige Punkte zur Vererbung in Java sind:

  • Basisklasse (Superklasse): Die bestehende Klasse, von der die Vererbung stattfindet, wird als Basisklasse oder Superklasse bezeichnet. Sie enthält allgemeine Eigenschaften und Methoden, die von den Unterklassen wiederverwendet werden können.
  • Unterklasse (abgeleitete Klasse): Die neu erstellte Klasse, die von der Basisklasse erbt, wird als Unterklasse oder abgeleitete Klasse bezeichnet. Die Unterklasse erbt die Eigenschaften und Methoden der Basisklasse und kann ihre eigenen spezifischen Eigenschaften und Methoden hinzufügen.
  • extends-Schlüsselwort: In Java wird die Vererbung mithilfe des extends-Schlüsselworts realisiert. Um eine Klasse von einer anderen Klasse erben zu lassen, deklarieren wir die Unterklasse und geben die Basisklasse mit dem extends-Schlüsselwort an.
  • Methodenüberschreibung (Override): Eine Unterklasse kann eine Methode aus der Basisklasse überschreiben, indem sie eine gleichnamige Methode mit derselben Signatur (Methodennamen und Parameterliste) in der Unterklasse definiert. Dies ermöglicht es der Unterklasse, das Verhalten der Methode zu ändern, die sie erbt.
  • Zugriffsmodifizierer: Die Zugriffsmodifizierer in der Basisklasse beeinflussen die Sichtbarkeit der vererbten Eigenschaften und Methoden in der Unterklasse. Die Unterklasse kann nur auf öffentliche und geschützte Elemente der Basisklasse zugreifen.

Hier ist ein einfaches Beispiel, um die Vererbung in Java zu verdeutlichen:


class Tier {
    protected String name;

    public Tier(String name) {
        this.name = name;
    }

    public void machenLaut() {
        System.out.println("Das Tier macht einen Laut.");
    }
}

// Unterklasse, die von der Basisklasse erbt
class Hund extends Tier {
    public Hund(String name) {
        super(name); // Aufruf des Konstruktors der Basisklasse mit 'super'
    }

    // Überschreiben der Methode machenLaut aus der Basisklasse
    @Override
    public void machenLaut() {
        System.out.println("Der Hund bellt: Wuff! Wuff!");
    }
}

public class Main {
    public static void main(String[] args) {
        // Erstellen eines Hund-Objekts
        Hund meinHund = new Hund("Bello");

        // Aufruf der Methoden aus der Basisklasse und Unterklasse
        System.out.println("Name des Tiers: " + meinHund.name);
        meinHund.machenLaut(); // Die überschriebene Methode in der Unterklasse wird aufgerufen
    }
}

Der abgebildete Quelltext zeigt die Klasse Mitarbeiter, die von der Klasse Person abgeleitet wird.
  • Die Klasse besitzt ein zusätzliches Attribut gehalt, das wie die Attribute der Basisklasse mit private gekapselt ist.
  • Die Getter-Methode getGehalt und die Setter-Methode setGehalt ermöglichen den Zugriff auf das Attribut gehalt.



Konstruktoren von abgeleiteten Klassen

Konstruktoren von abgeleiteten (Unter-) Klassen in Java können mithilfe des super()-Aufrufs den Konstruktor der Basisklasse (Superklasse) aufrufen, um die Initialisierung der geerbten Eigenschaften zu ermöglichen. Der super()-Aufruf muss immer als erstes Statement im Konstruktor der abgeleiteten Klasse stehen. Es gibt zwei Arten von Konstruktoraufrufen in abgeleiteten Klassen: Hier ist ein Beispiel, das die Verwendung von Konstruktoren in abgeleiteten Klassen verdeutlicht:

  • Expliziter Aufruf: Du kannst einen bestimmten Konstruktor der Basisklasse angeben, den du aufrufen möchtest. Dazu verwendest du super(parameter), wobei parameter die entsprechenden Werte für den aufgerufenen Basiskonstruktor sind.
  • Impliziter Aufruf: Wenn du keinen super()-Aufruf im Konstruktor der abgeleiteten Klasse angibst, ruft Java automatisch den Standardkonstruktor der Basisklasse (falls vorhanden) auf, der keine Parameter akzeptiert.

// Basisklasse
class Tier {
    protected String name;

    public Tier(String name) {
        this.name = name;
    }

    public void machenLaut() {
        System.out.println("Das Tier macht einen Laut.");
    }
}

// Abgeleitete Klasse, die von der Basisklasse erbt
class Hund extends Tier {
    private String rasse;

    // Konstruktor für die Unterklasse Hund
    public Hund(String name, String rasse) {
        super(name); // Aufruf des Konstruktors der Basisklasse Tier
        this.rasse = rasse;
    }

    // Überschreiben der Methode machenLaut aus der Basisklasse Tier
    @Override
    public void machenLaut() {
        System.out.println("Der Hund " + name + " bellt: Wuff! Wuff!");
    }

    // Neue Methode, spezifisch für die Klasse Hund
    public void rennen() {
        System.out.println("Der Hund " + name + " rennt.");
    }
}

public class Main {
    public static void main(String[] args) {
        // Erstellen eines Hund-Objekts
        Hund meinHund = new Hund("Bello", "Labrador");

        // Aufruf der Methoden der Unterklasse Hund
        meinHund.machenLaut(); // Die überschriebene Methode in der Unterklasse wird aufgerufen
        meinHund.rennen(); // Methode spezifisch für die Klasse Hund wird aufgerufen
    }
}


In diesem Beispiel haben wir eine Basisklasse Tier erstellt, die einen Konstruktor Tier(String name) und eine Methode machenLaut()enthält. Die abgeleitete Klasse Hund erbt von der Basisklasse Tier und fügt ein weiteres Feld rasse und eine spezifische Methode rennen() hinzu.

Der Konstruktor Hund(String name, String rasse) der Unterklasse Hund ruft den Konstruktor der Basisklasse Tier mithilfe von super(name) auf, um das Feld name der Basisklasse zu initialisieren. Anschließend initialisiert er das Feld rasse der Unterklasse.

Die Methode machenLaut() wird in der Unterklasse Hund überschrieben, um spezifisches Verhalten für Hunde zu definieren. Die Methode rennen() ist eine neue Methode, die spezifisch für die Klasse Hund ist und nicht in der Basisklasse Tier vorhanden ist.

Im main-Methode erstellen wir ein Hund-Objekt und rufen die Methoden machenLaut() und rennen() auf, um zu demonstrieren, wie die Vererbung und die Konstruktoren in abgeleiteten Klassen funktionieren.

Geerbte Methoden überschreiben

In Java können Methoden in abgeleiteten Klassen (Unterklassen) von der Basisklasse (Superklasse) überschrieben werden. Wenn eine Methode in der Unterklasse denselben Methodennamen, Rückgabetyp und Parameterliste wie die Methode in der Basisklasse hat, wird die Methode in der Unterklasse die Methode in der Basisklasse überschreiben. Hier ist ein Beispiel, das das Überschreiben von geerbten Methoden in Java zeigt:


// Basisklasse
class Tier {
    protected String name;

    public Tier(String name) {
        this.name = name;
    }

    public void machenLaut() {
        System.out.println("Das Tier macht einen Laut.");
    }
}

// Abgeleitete Klasse, die von der Basisklasse erbt
class Hund extends Tier {
    private String rasse;

    // Konstruktor für die Unterklasse Hund
    public Hund(String name, String rasse) {
        super(name);
        this.rasse = rasse;
    }

    // Überschreiben der Methode machenLaut aus der Basisklasse Tier
    @Override
    public void machenLaut() {
        System.out.println("Der Hund " + name + " bellt: Wuff! Wuff!");
    }

    // Neue Methode, spezifisch für die Klasse Hund
    public void rennen() {
        System.out.println("Der Hund " + name + " rennt.");
    }
}

public class Main {
    public static void main(String[] args) {
        // Erstellen eines Hund-Objekts
        Hund meinHund = new Hund("Bello", "Labrador");

        // Aufruf der Methode der Unterklasse Hund (überschrieben)
        meinHund.machenLaut(); // Die überschriebene Methode in der Unterklasse wird aufgerufen

        // Aufruf der Methode der Basisklasse Tier
        Tier meinTier = new Tier("Irgendein Tier");
        meinTier.machenLaut(); // Die Methode der Basisklasse wird aufgerufen
    }
}


In diesem Beispiel haben wir eine Basisklasse Tier mit der Methode machenLaut() erstellt. Die abgeleitete Klasse Hund erbt von der Basisklasse Tier und überschreibt die Methode machenLaut() mit ihrer eigenen Implementierung.

Im main-Methode erstellen wir ein Hund-Objekt und rufen die Methode machenLaut() auf. Da die Methode machenLaut() in der Klasse Hund überschrieben wurde, wird die spezifische Implementierung dieser Methode in der Unterklasse ausgeführt, und es wird "Der Hund Bello bellt: Wuff! Wuff!" auf der Konsole ausgegeben.

Wir erstellen auch ein Tier-Objekt und rufen ebenfalls die Methode machenLaut() auf. Da die Klasse Tier keine spezifische Implementierung der Methode machenLaut() hat und die Methode nicht überschrieben wurde, wird die Methode der Basisklasse ausgeführt und "Das Tier macht einen Laut." wird auf der Konsole ausgegeben.

Durch das Überschreiben von geerbten Methoden können Unterklasse spezifische Verhaltensweisen implementieren und das Verhalten der Basisklasse an ihre Bedürfnisse anpassen.

Polymorphie in der Vererbung

Polymorphie ist ein weiteres wichtiges Konzept der Vererbung in der objektorientierten Programmierung. Es ermöglicht es, dass ein Objekt einer abgeleiteten Klasse sich wie ein Objekt der Basisklasse verhält. Dadurch können verschiedene Objekte mit unterschiedlichen Implementierungen von geerbten Methoden gleich behandelt werden. Polymorphie erhöht die Flexibilität und Erweiterbarkeit von Code.

Es gibt zwei Arten von Polymorphie in Java:

  • Compilezeit-Polymorphie (statische Polymorphie): Dies tritt auf, wenn die Entscheidung, welche Methode aufgerufen werden soll, zur Compilezeit basierend auf dem statischen Typ (Referenztyp) des Objekts getroffen wird. Dies wird auch als "frühes Binden" bezeichnet.
  • Laufzeit-Polymorphie (dynamische Polymorphie): Dies tritt auf, wenn die Entscheidung, welche Methode aufgerufen werden soll, zur Laufzeit basierend auf dem dynamischen Typ (Objekttyp) des Objekts getroffen wird. Dies wird auch als "spätes Binden" bezeichnet.

Polymorphie wird oft durch das Überschreiben (Overriding) von Methoden in abgeleiteten Klassen erreicht. Wenn eine Methode in der Basisklasse überschrieben wird, kann sie in der Unterklasse mit einer spezifischeren Implementierung versehen werden. Während der Laufzeit wird die Methode, die aufgerufen wird, basierend auf dem tatsächlichen Typ des Objekts entschieden.

Hier ist ein Beispiel, das die Polymorphie in der Vererbung mit einer Klasse Tier und ihren abgeleiteten Klassen verdeutlicht:


// Basisklasse
class Tier {
    protected String name;

    public Tier(String name) {
        this.name = name;
    }

    public void machenLaut() {
        System.out.println("Das Tier macht einen Laut.");
    }
}

// Abgeleitete Klasse Hund, die von der Basisklasse Tier erbt
class Hund extends Tier {
    public Hund(String name) {
        super(name);
    }

    // Überschreiben der Methode machenLaut aus der Basisklasse Tier
    @Override
    public void machenLaut() {
        System.out.println("Der Hund " + name + " bellt: Wuff! Wuff!");
    }
}

// Abgeleitete Klasse Katze, die von der Basisklasse Tier erbt
class Katze extends Tier {
    public Katze(String name) {
        super(name);
    }

    // Überschreiben der Methode machenLaut aus der Basisklasse Tier
    @Override
    public void machenLaut() {
        System.out.println("Die Katze " + name + " miaut: Miau! Miau!");
    }
}

public class Main {
    public static void main(String[] args) {
        // Erstellen von Objekten der abgeleiteten Klassen
        Tier tier1 = new Hund("Bello");
        Tier tier2 = new Katze("Minka");

        // Aufrufen der überschriebenen Methode machenLaut
        tier1.machenLaut(); // Der Hund Bello bellt: Wuff! Wuff!
        tier2.machenLaut(); // Die Katze Minka miaut: Miau! Miau!
    }
}


In diesem Beispiel haben wir eine Basisklasse Tier mit einem Konstruktor Tier(String name) und der Methode machenLaut() erstellt. Die abgeleitete Klasse Hund und Katze erben von der Basisklasse Tier und überschreiben die Methode machenLaut() mit spezifischen Implementierungen.

Im main-Methode erstellen wir Objekte der abgeleiteten Klassen, aber sie werden als Referenzen des Basistyps (Tier) deklariert. Wenn wir die Methode machenLaut() aufrufen, wird die tatsächliche Implementierung zur Laufzeit basierend auf dem dynamischen Typ des Objekts (Hund oder Katze) aufgerufen. Dadurch erhalten wir das gewünschte Polymorphie-Verhalten, und sowohl Hunde als auch Katzen verhalten sich wie Tiere, aber mit unterschiedlichen spezifischen Implementierungen für das Geräusch, das sie machen.

Finale Klassen

In Java können Klassen als "final" deklariert werden, indem das Schlüsselwort "final" verwendet wird. Eine finale Klasse ist eine Klasse, von der keine anderen Klassen ableiten können, d.h., sie kann nicht als Basisklasse für andere Klassen verwendet werden. Das Schlüsselwort "final" wird verwendet, um zu verhindern, dass eine Klasse erweitert oder vererbt wird.

Hier sind einige wichtige Punkte über finale Klassen in Java:

  • Deklaration einer finalen Klasse:** Eine Klasse wird als final deklariert, indem das Schlüsselwort "final" vor dem Klassennamen steht. Zum Beispiel: `final class MeineKlasse { ... }`
  • Vererbung von finalen Klassen:** Eine finale Klasse kann nicht als Basisklasse für andere Klassen verwendet werden. Das bedeutet, dass keine andere Klasse von einer finalen Klasse erben kann.
  • Methodenüberschreibung in finalen Klassen:** In einer finalen Klasse können Methoden nicht mit dem Schlüsselwort "final" versehen werden, da diese sowieso nicht überschrieben werden können.
  • Variablen in finalen Klassen:** In einer finalen Klasse können Variablen als "final" deklariert werden, was bedeutet, dass sie einmal zugewiesen werden und danach nicht mehr geändert werden können.

Hier ist ein Beispiel für eine finale Klasse in Java:


final class MeineKlasse {
  // Eine finale Variable, die nur einmal zugewiesen werden kann
  final int MAX_ANZAHL = 100;

  // Eine Methode, die in einer finalen Klasse nicht überschrieben werden kann
  public final void meineMethode() {
  System.out.println("Diese Methode kann nicht überschrieben werden.");
  }
}

In diesem Beispiel haben wir eine finale Klasse `MeineKlasse` erstellt. Sie kann nicht von anderen Klassen erweitert werden. Die Methode `meineMethode()` ist mit "final" gekennzeichnet, was bedeutet, dass sie nicht in abgeleiteten Klassen überschrieben werden kann. Die Variable `MAX_ANZAHL` ist ebenfalls als "final" deklariert, was bedeutet, dass sie nur einmal zugewiesen werden kann.

Die Verwendung von finalen Klassen ist sinnvoll, wenn du sicherstellen möchtest, dass eine bestimmte Klasse nicht weiter vererbt oder erweitert wird, zum Beispiel aus Sicherheits- oder Designgründen. Finalität wird auch zur Leistungssteigerung genutzt, da der Compiler und der Laufzeitumgebung in der Lage sind, bestimmte Optimierungen für finale Klassen durchzuführen.

Instanceof Operator
Der instanceof Operator in Java wird verwendet, um zu überprüfen, ob ein Objekt eine Instanz einer bestimmten Klasse oder eines bestimmten Typs ist. Er wird häufig in der Vererbungshierarchie eingesetzt, um sicherzustellen, dass ein Objekt einem bestimmten Typ entspricht, bevor auf dessen Methoden oder Attribute zugegriffen wird. Dies kann helfen, ClassCastException Fehler zu vermeiden und den Code sicherer und robuster zu gestalten.


public class Tier {
    // Attribute und Methoden der Tierklasse
}

public class Hund extends Tier {
    // Attribute und Methoden der Hundklasse
}

public class Katze extends Tier {
    // Attribute und Methoden der Katzeklasse
}

public class Main {
    public static void main(String[] args) {
        Tier tier1 = new Hund();
        Tier tier2 = new Katze();

        if (tier1 instanceof Hund) {
            System.out.println("tier1 ist ein Hund");
        }

        if (tier2 instanceof Katze) {
            System.out.println("tier2 ist eine Katze");
        }

        if (tier1 instanceof Tier) {
            System.out.println("tier1 ist ein Tier");
        }

        if (tier2 instanceof Tier) {
            System.out.println("tier2 ist ein Tier");
        }
     
       if ( !(tier2 instanceof Hund) ) {
            System.out.println("tier2 ist kein Hund");
        }


 

Die Methoden clone und equals

Die Methoden clone() und equals() sind zwei wichtige Methoden in Java, die sich mit dem Kopieren (Klonen) von Objekten bzw. dem Vergleichen von Objekten befassen.

Die clone()-Methode ist Teil des Cloneable-Interfaces und wird verwendet, um eine flache Kopie eines Objekts zu erstellen. Das Cloneable-Interface dient als Markierungsschnittstelle, um anzuzeigen, dass eine Klasse das Klonen von Objekten unterstützt. Wenn eine Klasse das Cloneable-Interface implementiert, kann sie die clone()-Methode überschreiben, um ein Klonen von Objekten zu ermöglichen.

Es ist wichtig zu beachten, dass die clone()-Methode eine flache Kopie erstellt, d.h., sie kopiert nur die Referenzen auf die Felder des Objekts, nicht die Objekte selbst. Wenn das geklonte Objekt referenzierte Objekte enthält, teilen sich das Originalobjekt und das geklonte Objekt dieselben referenzierten Objekte. Daher kann dies manchmal zu unerwarteten Ergebnissen führen, wenn die referenzierten Objekte in einem der beiden Objekte geändert werden.

Hier ist ein Beispiel, das die Verwendung der clone()-Methode zeigt (wie im vorherigen Beispiel):


class Tier {
    private String name;
    private int alter;

    public Tier(String name, int alter) {
        this.name = name;
        this.alter = alter;
    }

    // Benutzerdefinierte Methode zum Klonen eines Tier-Objekts
    public Tier cloneTier() {
        return new Tier(this.name, this.alter);
    }

    // Überschreiben der equals()-Methode
    @Override
    public boolean equals(Object obj) {
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Tier tier = (Tier) obj;
        return alter == tier.alter && name.equals(tier.name);
    }

    public void anzeigen() {
        System.out.println("Name: " + name + ", Alter: " + alter);
    }
}

public class Main {
    public static void main(String[] args) {
        Tier tier1 = new Tier("Bello", 5);
        Tier tier2 = new Tier("Bello", 5);
        Tier tier3 = new Tier("Minka", 3);

        // Verwendung der equals()-Methode
        System.out.println(tier1.equals(tier2)); // Ausgabe: true
        System.out.println(tier1.equals(tier3)); // Ausgabe: false

        // Verwendung der benutzerdefinierten cloneTier()-Methode
        Tier tierClone = tier1.cloneTier();

        // Ändern des geklonten Objekts
        tierClone.alter = 7;
        tierClone.anzeigen(); // Ausgabe: Name: Bello, Alter: 7

        // Das ursprüngliche Objekt bleibt unverändert
        tier1.anzeigen(); // Ausgabe: Name: Bello, Alter: 5
    }
}



In diesem Beispiel haben wir die Klasse Tier ohne das Cloneable-Interface erstellt und stattdessen eine benutzerdefinierte Methode cloneTier() implementiert, die ein neues Tier-Objekt mit denselben Werten für die Felder name und alter erstellt und zurückgibt. Die equals()-Methode wurde überschrieben, um die Objekte anhand ihrer name- und alter-Felder zu vergleichen. Das Klonen eines Objekts erfolgt jetzt über die benutzerdefinierte Methode cloneTier() und ermöglicht es, eine Kopie des Objekts zu erstellen, ohne das Cloneable-Interface zu verwenden. Beachte, dass dies immer noch eine flache Kopie ist und Referenzen auf andere Objekte nicht geklont werden. Wenn du eine tiefe Kopie benötigst, müsstest du die entsprechenden Referenzen ebenfalls klonen.

Abstrakte Klassen und Methoden

Abstrakte Klassen und Methoden sind wichtige Konzepte in der objektorientierten Programmierung, die in Java verwendet werden, um die Struktur von Klassen und die Implementierung von Methoden zu definieren.

Abstrakte Klassen:

Eine abstrakte Klasse ist eine Klasse, die nicht direkt instanziiert werden kann, d.h., du kannst kein Objekt einer abstrakten Klasse erstellen. Stattdessen dient eine abstrakte Klasse als Vorlage oder Grundgerüst für andere Klassen, die von ihr erben (Subklassen). Eine abstrakte Klasse kann sowohl abstrakte Methoden als auch konkrete Methoden enthalten.

Deklaration: Um eine Klasse als abstrakt zu kennzeichnen, verwendest du das Schlüsselwort abstract vor der Klassendeklaration. Zum Beispiel: abstract class MeineAbstrakteKlasse { ... }

Abstrakte Methoden:

Eine abstrakte Methode ist eine Methode, die in der abstrakten Klasse deklariert wird, aber keine Implementierung enthält. Sie hat nur die Methode-Signatur, d.h., die Methodenname, die Parameterliste und den Rückgabetyp, aber keinen Methodenrumpf. Die Implementierung der abstrakten Methode wird den abgeleiteten Klassen überlassen. Alle abgeleiteten Klassen müssen die abstrakten Methoden der Basisklasse überschreiben und ihnen eine eigene Implementierung geben.

Deklaration: Um eine Methode als abstrakt zu kennzeichnen, verwendest du das Schlüsselwort abstract vor der Methodendeklaration und setzt einen Semikolon anstelle eines Methodenrumpfs. Zum Beispiel: public abstract void meineMethode();

Hier ist ein Beispiel für eine abstrakte Klasse `Tier` mit einer abstrakten Methode `lautGeben()`:


abstract class Tier {
  protected String name;
  protected int alter;

  public Tier(String name, int alter) {
  this.name = name;
  this.alter = alter;
  }

  // Abstrakte Methode ohne Implementierung
  public abstract void lautGeben();

  public void anzeigen() {
  System.out.println("Name: " + name + ", Alter: " + alter);
  }
  }

  // Abgeleitete Klasse, die von der abstrakten Klasse Tier erbt
  class Hund extends Tier {
  public Hund(String name, int alter) {
  super(name, alter);
  }

  // Überschreiben der abstrakten Methode lautGeben
  @Override
  public void lautGeben() {
  System.out.println("Der Hund " + name + " bellt: Wuff! Wuff!");
  }
  }

  // Abgeleitete Klasse, die von der abstrakten Klasse Tier erbt
  class Katze extends Tier {
  public Katze(String name, int alter) {
  super(name, alter);
  }

  // Überschreiben der abstrakten Methode lautGeben
  @Override
  public void lautGeben() {
  System.out.println("Die Katze " + name + " miaut: Miau! Miau!");
  }
  }

  public class Main {
  public static void main(String[] args) {
  Tier tier1 = new Hund("Bello", 5);
  Tier tier2 = new Katze("Minka", 3);

  tier1.anzeigen(); // Ausgabe: Name: Bello, Alter: 5
  tier1.lautGeben(); // Ausgabe: Der Hund Bello bellt: Wuff! Wuff!

  tier2.anzeigen(); // Ausgabe: Name: Minka, Alter: 3
  tier2.lautGeben(); // Ausgabe: Die Katze Minka miaut: Miau! Miau!
  }
}

In diesem Beispiel haben wir die abstrakte Klasse `Tier` erstellt, die eine abstrakte Methode `lautGeben()` enthält, die in den abgeleiteten Klassen `Hund` und `Katze` überschrieben wird. Die abstrakte Klasse `Tier` enthält auch die Felder `name` und `alter`, sowie eine konkrete Methode `anzeigen()`, die eine Implementierung hat und von den abgeleiteten Klassen verwendet werden kann.

Die abstrakte Methode `lautGeben()` definiert das Verhalten des Lautgebens für verschiedene Tierarten, wird jedoch in der abstrakten Klasse nicht selbst implementiert, da das Geräusch von Hund und Katze unterschiedlich ist. Stattdessen wird die Methode in den abgeleiteten Klassen `Hund` und `Katze` überschrieben, um das spezifische Lautgeben der jeweiligen Tiere zu implementieren.

Beachte, dass die abstrakte Klasse `Tier` nicht direkt instanziiert werden kann. Stattdessen erstellen wir Objekte der abgeleiteten Klassen `Hund` und `Katze`, die von der abstrakten Klasse `Tier` erben, und rufen die Methoden der abstrakten Klasse über die abgeleiteten Objekte auf. Dadurch erhalten wir das gewünschte Polymorphie-Verhalten, wo verschiedene Tiere unterschiedliche Geräusche machen, aber alle dieselbe Methode `anzeigen()` verwenden.


Aufgabenstellung1:


Erstellen Sie eine Basisklasse Tier und mehrere abgeleitete Klassen, die von Tier erben. Implementieren Sie verschiedene Methoden in der Basisklasse und überschreiben Sie diese Methoden in den abgeleiteten Klassen. Fügen Sie in den abgeleiteten Klassen das zusätzliche Attribut geschlecht hinzu. Erstellen Sie Objekte dieser Klassen und rufen Sie die Methoden auf, um die Polymorphie zu demonstrieren.

Basisklasse Tier:

Attribute:
name (Typ: String)
alter (Typ: int)

Konstruktor:
Ein Konstruktor, der name und alter als Parameter nimmt und die Attribute initialisiert.

Methoden:
makeSound(): Eine Methode, die eine generische Tierlautmeldung ausgibt.
printDetails(): Eine Methode, die die Details des Tieres ausgibt.

Abgeleitete Klassen:

Hund
Zusätzliches Attribut Rase (Typ: String)
Konstruktor, der name, alter und geschlecht als Parameter nimmt und alle Attribute initialisiert.
Überschreibt die Methode makeSound(), um "Wuff" auszugeben.
Überschreibt die Methode printDetails(), um zusätzlich das Geschlecht auszugeben.

Katze
Zusätzliches Attribut Geschlecht (Typ: String)
Konstruktor, der name, alter und geschlecht als Parameter nimmt und alle Attribute initialisiert.
Überschreibt die Methode makeSound(), um "Miau" auszugeben.
Überschreibt die Methode printDetails(), um zusätzlich das Geschlecht auszugeben.

Vogel
Zusätzliches Attribut Frabe (Typ: String)
Konstruktor, der name, alter und geschlecht als Parameter nimmt und alle Attribute initialisiert.
Überschreibt die Methode makeSound(), um "Piep" auszugeben.
Überschreibt die Methode printDetails(), um zusätzlich das Geschlecht auszugeben.

Main-Methode:
Erstellen Sie Objekte der Klassen Hund, Katze und Vogel.
Speichern Sie diese Objekte in einem Array vom Typ Tier.
Iterieren Sie über das Array und rufen Sie die Methoden printDetails() und makeSound() für jedes Objekt auf.