Home > Informatik > Einführung in die OOP > 9. Vererbung > 9.1 Eine Buchliste mit Vererbung

9.1 Eine Buchliste mit Vererbung

9.1.1 Zielsetzung

Wir wollen ein ArrayList-Objekt erstellen, in dem wir Objekte der Klasse Buch speichern können. An diesem Beispiel wollen wir dann das grundlegende Prinzip der Vererbung kennenlernen.

9.1.2 Die Klassen Buch und Buchliste

Die Klasse Buch - Version 1

Quelltext

public class Buch
// Version 1
{
    private String titel, autor;
    private int jahr;
    
    public Buch(String titel, String autor, int jahr)
    {
        this.titel  = titel;
        this.autor = autor;
        this.jahr   = jahr;
    }
    
    public void zeige()
    {
        System.out.println("---------------------------------");
        System.out.println("Titel : " + titel);
        System.out.println("Autor : " + autor);
        System.out.println("Jahr  : " + jahr);
        System.out.println("_________________________________/");
        System.out.println();
    }
}

Quelltext: Buch / Buch(erweitert)

Diese erste Version der Klasse Buch ist noch recht einfach aufgebaut: Drei Instanzvariablen, ein Konstruktor, der diese Instanzvariablen initialisiert, und eine Methode zeige(). Es handelt sich hier um einen Minimal-Quelltext. Im nächsten Schritt wollen wir Getter-Methoden und überprüfende Setter-Methoden ergänzen.

Checkende Setter-Methoden

Beispiel setAutor()
public void setAutor(String autor)
{
   if (autor == null || autor.isBlank())
      throw new IllegalArgumentException("Der Autor darf nicht leer sein.");
   this.autor = autor;
}

Die Setter-Methode für den Titel sieht ähnlich aus, und bei der Setter-Methode für das Jahr müssen wir die Jahreszahl überprüfen, ob sie gültig ist.

Mit solchen kontrollierenden Setter-Methoden können wir auch den Konstruktor der Klasse etwas umgestalten:

public Buch(String titel, String autor, int jahr)
{
   setTitel(titel);
   setAutor(autor);
   setJahr(jahr);
}

Getter-Methoden

Beispiel getAutor()

public String getAutor()
{
   return autor;
}

Bei den Getter-Methoden müssen wir keine weiteren Prüfungen durchführen, daher sind sie recht einfach gestaltet.

Vollständiger Quelltext

Den vollständigen Quelltext mit Getter- und überprüfenden Setter-Methoden finden Sie auf dieser Seite.

Die Klasse Buchliste, Version 1

Schauen wir uns nun an, wie man Buch-Objekte in einem ArrayList-Objekt speichern kann.

Quelltext

import java.util.ArrayList;

public class Buchliste
// Version 1
{
    ArrayList  liste;

    public Buchliste()
    {
        liste = new ArrayList<>();
        
        erzeuge();
        zeige();
    }

    public void erzeuge()
    {
        liste.add(new Buch("Einführung in Java","Meier, Otto",2025));
        liste.add(new Buch("Lexikon der Informatik","Duden-Verlag",2012));
        liste.add(new Buch("Der Herr der Ringe","J.R. Tolkien",1964));
        liste.add(new Buch("Prinzipien der OOP","Müller, Jonathan",2020));
    }

    public void zeige()
    {
       for (Buch b: liste)
       {
          b.zeige(); 
       }
    }
    
    public static void main(String[] args)
    {
       new Buchliste();
    }
}

Quelltext: Buchliste / Buchliste (erweitert)

Die Verwendung von ArrayList<Buch> stellt sicher, dass nur Objekte der Klasse Buch in dieser Liste gespeichert werden können (Typsicherheit).

Der Konstruktor initialisiert die Liste und ruft anschließend die Methoden erzeuge() und zeige() auf.

In der Methode erzeuge() werden mehrere Buch-Objekte erzeugt und der Liste hinzugefügt. Eine Schleife ist hierfür nicht sinnvoll, da die Objekte unterschiedliche Attributwerte besitzen und daher individuell konstruiert werden müssen.

Die Methode zeige() verwendet dagegen eine for-each-Schleife, um alle Elemente der Liste zu durchlaufen. Für jedes Buch-Objekt wird die Ausgabe an die Methode zeige() der Klasse Buch delegiert. Eine Schleife ist hierfür perfekt geeignet, da alle Objekte nach dem gleichen Schema verarbeitet werden.

Wenn wir statt der offiziellen Klasse ArrayList die eigene Klasse MyArrayList benutzen, die wir in Folge 8 selbst entwickelt haben, können wir bei zeige() keine for-each-Schleife einsetzen, da MyArrayList das Interface Iterator nicht implementiert hat.

Die main()-Methode erzeugt lediglich ein Objekt der Klasse Buchliste und startet damit das Programm.

Quelltext (Version mit Exception-Handling)

Wir ergänzen die erzeuge()-Methode mit einem try-catch-Konstrukt:

public void erzeuge()
{
    try
    {
        liste.add(new Buch("Einführung in Java", "Meier, Otto", 2025));
        liste.add(new Buch("Lexikon der Informatik", "Duden-Verlag", 2012));
        liste.add(new Buch("Der Herr der Ringe", "J.R. Tolkien", 1964));
        liste.add(new Buch("Prinzipien der OOP", "Müller, Jonathan", 2020));
    }
    catch (IllegalArgumentException e)
    {
        System.out.println("Fehler beim Erzeugen eines Buches:");
        System.out.println(e.getMessage());
    }
}

So könnte eine Version der Klasse Buchliste mit Exception-Handling aussehen. Ein Problem besteht allerdings noch bei der Methode erzeuge(): Wenn bei der Erzeugung des ersten Buchs eine Exception auftritt, wird die ganze erzeuge()-Methode sofort abgebrochen und der catch-Block wird ausgeführt. Die noch folgenden Bücher werden der Liste nicht mehr hinzugefügt.

Eine noch bessere Version von Buchliste besitzt eine eigene fuegeBuchHinzu()-Methode:

private void fuegeBuchHinzu(String titel, String autor, int jahr)
{
   try
   {
      liste.add(new Buch(titel, autor, jahr));
   }
   catch (IllegalArgumentException e)
   {
      System.out.println("Fehler beim Hinzufügen eines Buches:");
      System.out.println(e.getMessage());
   }
}

Die Methode erzeuge() ruft jetzt für jedes Buch diese neue Methode auf. Sollte dabei eine Exception geworfen werden, wird diese in fuegeBuchHinzu() nur für dieses eine Buch behandelt, die anderen Bücher können dann trotzdem hinzugefügt werden.

Den vollständigen Quelltext der erweiterten Buchliste können Sie hier herunterladen.

Die Konsolenausgabe

---------------------------------
Titel : Einführung in Java
Autor : Meier, Otto
Jahr  : 2025
_________________________________/

---------------------------------
Titel : Lexikon der Informatik
Autor : Duden-Verlag
Jahr  : 2012
_________________________________/

---------------------------------
Titel : Der Herr der Ringe
Autor : J.R. Tolkien
Jahr  : 1964
_________________________________/

---------------------------------
Titel : Prinzipien der OOP
Autor : Müller, Jonathan
Jahr  : 2020
_________________________________/

Die Konsolenausgabe sieht recht gut aus - für ein reines Text-Programm. Im weiteren Verlauf des Projektes werden wir diese Konsolenausgabe aber noch weiter verfeinern.

9.1.3 Spezialisierte Buch-Klassen

Version 2 des Buch-Projekts

Das Objekt ArrayList<Buch> liste bzw. MyArrayList<Buch> liste soll jetzt weitere Buch-Klassen speichern können, beispielsweise Objekte der Klassen Roman und Sachbuch. In dieser Folge 9 wird das Prinzip der Vererbung vorgestellt.

Wir wollen aber zunächst einmal sehen, wie wir eine solche heterogene Liste implementieren können, wenn wir den Mechanismus der Vererbung noch nicht kennen und benutzen. In Zukunft wollen wir eine solche Implementierung jedoch nicht mehr anwenden, daher heißt der nächste Abschnitt:

Wie man es nicht machen sollte

Eine naheliegende Lösung wäre es, die Klasse Buch per Copy & Paste zu duplizieren und dann in eigene Klassen wie Roman und Sachbuch umzubenennen.

Diese Klassen müssten anschließend unterschiedlich erweitert werden, zum Beispiel um eine Instanzvariable genre (für Romane) bzw. fachgebiet (für Sachbücher). Auch die Methode zeige() müsste in jeder Klasse speziell angepasst werden.

Ein solcher Ansatz ist zwar kurzfristig umsetzbar (und passiert in der Praxis auch recht häufig), führt aber zu erheblichen Nachteilen, wie wir später noch sehen werden.

Schauen wir uns an, was passiert, wenn man die Klasse Buch kopiert und dann so verändert, dass man eine neue Klasse Roman erhält:

public class Roman
{
    private String titel, autor;
    private int jahr;
    private String genre;
    
    public Roman(String titel, String autor, String genre, int jahr)
    {
        this.titel  = titel;
        this.autor = autor;
        this.genre  = genre;
        this.jahr   = jahr;
    }
    
     public void zeige()
    {
        System.out.println("---------------------------------");
        System.out.println("Titel : " + titel);
        System.out.println("Autor : " + autor);
        System.out.println("Genre : " + genre);
        System.out.println("Jahr  : " + jahr);
        System.out.println("---------------------------------");
        System.out.println();
    }
}

Hier wurde übrigens nur die Basis-Version der Klasse Buch als Kopiervorlage verwendet, also die Version ohne Getter- und checkenden Setter-Methoden. Wir wollen den Rahmen dieser Seite ja nicht sprengen, die sowieso schon recht lang ist.

Die Klasse Sachbuch erzeugen wir auf die gleiche Weise, die Instanzvariable genre wird dann durch eine Instanzvariable fachgebiet ersetzt, entsprechend müssen wir den Konstruktor und die zeige()-Methode etwas ändern.

Warum man es so nicht machen sollte

Die drei Klassen Buch, Roman und Sachbuch unterscheiden sich nur in wenigen Details, enthalten aber ansonsten nahezu identischen Code. Diese Code-Duplizierung sollte grundsätzlich vermieden werden.

Angenommen, Sie wollen bei der Ausgabe der Bücher die Reihenfolge der Attribute ändern. Bisher wird zuerst der Titel ausgegeben, dann der Autor und am Ende das Erscheinungsjahr. Sie wollen nun aber die Ausgabe so ändern, dass zuerst der Autor ausgegeben wird, dann der Titel und das Erscheinungsjahr.

Dann müssen Sie in jeder der drei Klassen die zeige()-Methode anpassen - das ist ein hoher Wartungsaufwand wegen der vorliegenden Redundanz.

Außerdem kann es passieren, dass Sie bei den Anpassungen eine der drei (oder mehr) Klassen vergessen - hohe Fehleranfälligkeit.

Ein weiteres Beispiel: Sie wollen nicht nur Autor, Titel und Erscheinungsjahr in den Objekten der drei Klassen speichern, sondern ein weiteres Attribut, beispielsweise die Seitenzahl. Dann müssen Sie dieses Attribut in jeder der drei Klassen als weitere Instanzvariable deklarieren, eine entsprechende Setter- und Getter-Methode schreiben und auch die zeige()-Methoden in jeder Klasse anpassen.

Wie leicht passiert es, dass man diese Änderungen bei zwei Klassen durchführt, aber bei der dritten vergisst oder auf eine andere Weise durchführt, die nicht zu den bisherigen Änderungen passt?

Das bisherige Vorgehen - das man leider in der Praxis viel zu oft findet - hat mehrere Nachteile:

  1. Redundanz: Große Teile des Quelltexts sind identisch und mehrfach vorhanden.
  2. Wartungsaufwand: Änderungen müssen an mehreren Stellen durchgeführt werden.
  3. Fehleranfälligkeit: Inkonsistenzen zwischen den Klassen sind leicht möglich.

Ein weiteres Problem

Abgesehen von diesen Nachteilen ist das oben beschriebene Vorgehen mit einem weiteren Problem verbunden. In einer typisierten Liste wie

ArrayList <Buch> liste;

können nur Objekte der Klasse Buch gespeichert werden. Objekte der Klassen Roman und Sachbuch wären nicht kompatibel zum Typ-Parameter<Buch> und könnten daher nicht in der Liste gespeichert werden.

Vordergründige Lösung dieses Problems

Man könnte natürlich einfach auf Generics verzichten und schreiben:

ArrayList liste;

statt

ArrayList<Buch> liste;

Jetzt könnte man Objekte aller drei Klassen Buch, Roman und Sachbuch in der ArrayList speichern, man würde so eine heterogene Liste erzeugen. Allerdings gingen bei diesem Vorhaben wichtige Vorteile verloren:

  • Es gibt keine Typsicherheit mehr.
  • Methoden wie get() liefern nur noch Objekte der Klasse Object.
  • Ein Zugriff erfordert daher explizites Typecasting und gegebenenfalls Typüberprüfungen mit dem instanceof-Operator.
Wie sähe eine heterogene Buchliste ohne Generics aus?

Würde man bei der Klasse Buchliste auf Generics verzichten, sähe die for-Schleife der zeige()-Methode so aus:

for (int i = 0; i < liste.size(); i++)
{
    Object objekt = liste.get(i);

    if (objekt instanceof Roman)
    {
       Roman roman = (Roman) objekt;
       roman.zeige();
    }
    else if (objekt instanceof Sachbuch)
    {
       Sachbuch sachbuch = (Sachbuch) objekt;
       sachbuch.zeige();
    }
    else if (objekt instanceof Buch)
    {
       Buch buch = (Buch) objekt;
       buch.zeige();
    }
}

Mit

objekt instanceof Klasse

überprüft man, ob das gegebene Objekt der jeweiligen Klasse angehört. Wenn das der Fall ist, wenn also beispielsweise das aktuelle Objekt der Klasse Roman angehört, muss das Objekt, das von get() geliefert wurde, per Typecasting in ein Roman-Objekt umgewandelt werden.

    if (objekt instanceof Roman)
    {
       Roman roman = (Roman) objekt;
       roman.zeige();
    }

Verzichtet man auf Generics, so gehen Typsicherheit und Komfort verloren: Beim Auslesen aus einer Liste sind dann oft instanceof-Prüfungen und explizites Typecasting notwendig.

"Explizites Typecasting" heißt, dass das Typecasting nicht automatisch vom Compiler durchgeführt wird, sondern von Entwickler bewusst eingesetzt wird. Explizites Typecasting erkennt man an dem Datentyp, der in runden Klammern vor dem umzuwandelnden Datentyp steht.

➥Quelltexte des Projekts "Buchliste-2"

Buch.java - Roman.java - Sachbuch.java - Buchliste.java

Dieses Java-Projekt zeigt die Implementation einer Buchliste ohne Verwendung von Generics - also wie man es nicht machen sollte (aber durchaus kann).

Generalisierung und Spezialisierung

In der objektorientierten Programmierung kennt man zwei wichtige und eng zusammenhängende Prinzipien: Generalisierung und Spezialisierung.

Generalisierung bedeutet, gemeinsame Eigenschaften und Verhaltensweisen mehrerer Klassen zu identifizieren und in einer Oberklasse zusammenzufassen.

Im unserem Buchliste-Beispiel werden die Gemeinsamkeiten der Klassen Roman und Sachbuch – etwa titel, autor und jahr sowie die Methode zeige() – in die Oberklasse Buch ausgelagert.

Spezialisierung beschreibt den umgekehrten Weg: Ausgehend von einer allgemeinen Klasse werden speziellere Unterklassen gebildet, die zusätzliche Eigenschaften (also Instanzvariablen) oder angepasstes Verhalten (also Methoden) besitzen.

So entstehen aus der Klasse Buch die Unterklassen Roman und Sachbuch, die jeweils um spezifische Instanzvariablen wie genre bzw. fachgebiet erweitert werden.

Seitenanfang
Weiter mit dem Konzept der Vererbung ...