2.06 Vererbung

Einführung

Sehen wir uns einmal diese beiden Klassen an:

Klassendiagramme von Lehrer und Schueler.
Zwei Klassen mit vielen Gemeinsamkeiten.

Wie Dir sicherlich auffällt, haben beide Klassen viele Gemeinsamkeiten. Sowohl ein Lehrer als auch ein Schüler haben jeweils einen Namen sowie ein Geburtsjahr. Das wundert uns nicht, denn schließlich hat ja jede Person auf der Welt – nicht nur Lehrer und Schüler – einen Namen und ein Geburtsjahr. Da sowohl  Lehrer als auch Schüler nun einmal Personen sind, teilen sie sich diese Gemeinsamkeiten.

Diese Gemeinsamkeiten kommen bei der objektorientierten Programmierung besonders zur Geltung. Die Tatsache, dass Lehrer und Schüler spezielle Arten von Personen darstellen, modellieren wir mit einer gemeinsamen Oberklasse Person:

Von den Klassen Lehrer und Schueler zeigt jeweils ein Pfeil auf eine Klasse Person.
Die beiden Klassen besitzen eine gemeinsame Oberklasse.

Man sagt, die Klassen Lehrer und Schueler sind Spezialisierungen der Klasse Person. Umgekehrt ist die Klasse Person eine Generalisierung der Klassen Lehrer und Schueler.

Beachte, dass die Attribute und Methoden, die schon in der Klasse Person vorhanden sind, nicht noch einmal in den Klassen Lehrer und Schueler aufgelistet werden. Es ist aus dem Diagramm auch so erkenntlich, dass sie diese besitzen. Man sagt, dass sie diese Attribute und Methoden von der Klasse Person erben. Deswegen spricht man bei dieser Art von Beziehung zwischen den Klassen auch von Vererbung.

Sehen wir uns an, wie wir diese Beziehung in Java umsetzen. Zunächst beginnen wir mit der Klasse Person:

public class Person {

  // Attribute
  private String name;
  private int geburtsjahr;
  
  // Konstruktor
  public Person(String pName, int pGeburtsjahr){
    name = pName;
    geburtsjahr = pGeburtsjahr;
  }

  // get-Methoden
  public String getName(){
    return name;
  }

  public int getGeburtsjahr(){
    return geburtsjahr;
  }

}

In dieser Klasse passiert noch nichts ungewöhnliches. Erst bei der Klasse Lehrer sehen wir zwei Besonderheiten:

public class Lehrer extends Person {

  // Attribute
  private String erstesFach;
  private String zweitesFach;

  // Konstruktor
  public Lehrer(String pName, int pGeburtsjahr, String pErstesFach, String pZweitesFach){
    super(pName,pGeburtsjahr);
    erstesFach = pErstesFach;
    zweitesFach = pZweitesFach;
  }

  // get-Methoden
  public String getErstesFach(){
    return erstesFach;
  }

  public String getZweitesFach(){
    return zweitesFach;
  }

}

Das Schlüsselwort extends gibt an, dass diese Klasse eine Spezialisierung einer anderen Klasse ist. Genauer gesagt, sehen wir hier, dass die Klasse Lehrer eine Spezialisierung von Person ist.

Die zweite Besonderheit liegt im Konstruktor vor. Zunächst verlangt dieser wie gewöhnlich eine Reihe von Parametern. Neu ist für uns dann aber die Anweisung super (pName , pGeburtsjahr );. Diese ist wie folgt zu verstehen. Mit dem Schlüsselwort super können wir uns immer auf die Oberklasse einer Klasse beziehen. In diesem Fall beziehen wir uns also auf Person. Um genau zu sein, rufen wir an dieser Stelle den Konstruktor von Person auf. Diesem Konstruktor übergeben wir die beiden Parameter pName und pGeburtsjahr, damit er diese dann weiterverarbeitet. Die Parameter pErstesFach und pZweitesFach hingegen können wir nicht an ihn weiterreichen, da die entsprechenden Attribute in der Klasse Person nicht existieren. Daher werden diese im Konstruktor der Klasse Lehrer den Attributen zugeordnet.

Sehen wir uns ein kleines Beispiel für das Erstellen eines Objektes der Klasse Lehrer an:

public class Lehrertest {

  public static void main(String[] args) {
    
    Lehrer meinLehrer;
    meinLehrer = new Lehrer("Meier", 1982, "Mathe", "Bio");
    
    System.out.println(meinLehrer.getName());

  }

}

Als Ausgabe erhalten wir hier den Namen des Lehrers. Dabei sollte uns auffallen, dass die Klasse Lehrer gar keine Methode getName() besitzt. Das ist aber kein Problem, da sie diese Methode von der Klasse Person erbt.

Die Klasse Schueler kannst Du nun selbst zur Übung einmal erstellen. Hast Du die Klasse Lehrer gut verstanden, wird das kein Problem für Dich sein 🙂

Überschreiben von Methoden

Manchmal kommt es vor, dass eine Oberklasse bereits eine Methode besitzt, die wir auch in einer Unterklasse verwenden wollen, allerdings in einer anderen Weise. Dies sehen wir uns an einem ganz kleinen Beispiel an. Dazu fügen wir der Klasse Person zunächst eine Methode schreibeInfo() hinzu, die nur die Aufgabe hat, in der Konsole einige Angaben zu machen:

public class Person {

  // Attribute
  private String name;
  private int geburtsjahr;
  
  // Konstruktor
  public Person(String pName, int pGeburtsjahr){
    name = pName;
    geburtsjahr = pGeburtsjahr;
  }

  // get-Methoden
  public String getName(){
    return name;
  }

  public int getGeburtsjahr(){
    return geburtsjahr;
  }
  
  // Ausgabe in Konsole
  public void schreibeInfo(){
    System.out.println("Mein Name ist "+name+".");
    System.out.println("Ich bin im Jahr "+geburtsjahr+" geboren.");
  }

}

Wir können uns direkt davon überzeugen, dass diese Methode funktioniert:

public class Persontest {

  public static void main(String[] args) {
    
    Person meinePerson;
    meinePerson = new Person("Schmitz", 1975);
    meinePerson.schreibeInfo();    
    
  }

}

Dieser kleine Test liefert die folgende Ausgabe:

Mein Name ist Schmitz.
Ich bin im Jahr 1975 geboren.

Der Klasse Lehrer wollen wir nun auch eine Methode schreibeInfo() hinzufügen. Bei dieser sollen aber auch noch die Fächer des Lehrers angegeben werden. Eine Möglichkeit wäre diese:

public class Lehrer extends Person {

  // Attribute
  private String erstesFach;
  private String zweitesFach;

  // Konstruktor
  public Lehrer(String pName, int pGeburtsjahr, String pErstesFach, String pZweitesFach){
    super(pName,pGeburtsjahr);
    erstesFach = pErstesFach;
    zweitesFach = pZweitesFach;
  }

  // get-Methoden
  public String getErstesFach(){
    return erstesFach;
  }

  public String getZweitesFach(){
    return zweitesFach;
  }
  
  // Ausgabe in Konsole
  public void schreibeInfo(){
    System.out.println("Mein Name ist "+getName()+".");
    System.out.println("Ich bin im Jahr "+getGeburtsjahr()+" geboren.");
    System.out.println("Ich unterrichte "+erstesFach+" und "+zweitesFach+".");
  }

}

Beachte ein Detail: Da die Attribute name und geburtsjahr in Person als private deklariert sind, können wir nur über die Getter an sie herankommen. Eine Alternative gucken wir uns noch später an.

Wenn wir nun einen Lehrer erstellen und ihm die Anweisung schreibeInfo() geben, wird diese neue Methode der Klasse Lehrer aufgerufen. Die entsprechende Methode der Klasse Person wird ignoriert. Zum Beispiel liefert dieser Test

public class Lehrertest {

  public static void main(String[] args) {
    
    Lehrer meinLehrer;
    meinLehrer = new Lehrer("Meier", 1982, "Mathe", "Bio");
    
    meinLehrer.schreibeInfo();

  }

}

die folgende Ausgabe:

Mein Name ist Meier.
Ich bin im Jahr 1982 geboren.
Ich unterrichte Mathe und Bio.

Uns sollte nun auffallen, dass wir in den ersten zwei Zeilen der neuen Methode zunächst die Methode schreibeInfo() der Klasse Person im Grunde wiederholen und anschließend noch die beiden Fächer ausgeben. Das geht eleganter:

public class Lehrer extends Person {

  // Attribute
  private String erstesFach;
  private String zweitesFach;

  // Konstruktor
  public Lehrer(String pName, int pGeburtsjahr, String pErstesFach, String pZweitesFach){
    super(pName,pGeburtsjahr);
    erstesFach = pErstesFach;
    zweitesFach = pZweitesFach;
  }

  // get-Methoden
  public String getErstesFach(){
    return erstesFach;
  }

  public String getZweitesFach(){
    return zweitesFach;
  }
  
  // Ausgabe in Konsole
  public void schreibeInfo(){
    super.schreibeInfo();
    System.out.println("Ich unterrichte "+erstesFach+" und "+zweitesFach+".");
  }

}

In dieser Variante verwenden wir das, was wie bereits vorher programmiert haben erneut. Mit super.schreibeInfo(); rufen wir die Methode schreibeInfo() der Oberklasse Person auf. Nachdem diese Name und Geburtsjahr ausgegeben hat, folgen noch die Angaben über die Fächer. Die Ausgabe ist insgesamt also dieselbe wie vorher.

Die Methode schreibeInfo() hat in der Unterklasse Lehrer also nun eine neue Bedeutung erhalten. Wir haben, wie man sagt, diese Methode überschrieben. Wir hätten sie dabei auch komplett umfunktionieren können, aber hier schien es sinnvoll, ihre Funktion nur ein wenig zu erweitern.

protected statt private

Wir haben oben bereits in einem Beispiel gesehen, dass wir von der Unterklasse Lehrer nicht auf die Attribute name und geburtsjahr der Oberklasse Person zugreifen können, da diese als private deklariert sind. Sehen wir uns noch ein Beispiel an, bei dem dies etwas störend ist:

public class Lehrer extends Person {

  // Attribute
  private String erstesFach;
  private String zweitesFach;

  // Konstruktor
  public Lehrer(String pName, int pGeburtsjahr, String pErstesFach, String pZweitesFach){
    super(pName,pGeburtsjahr);
    erstesFach = pErstesFach;
    zweitesFach = pZweitesFach;
  }

  // get-Methoden
  public String getErstesFach(){
    return erstesFach;
  }

  public String getZweitesFach(){
    return zweitesFach;
  }
  
  // Ausgabe in Konsole
  public void schreibeInfo(){
    super.schreibeInfo();
    System.out.println("Ich unterrichte "+erstesFach+" und "+zweitesFach+".");
    System.out.println("Im Jahr "+(getGeburtsjahr()+67)+ " gehe ich in den Ruhestand.");
  }

}

Die Methode schreibeInfo() wurde hier so erweitert, dass auch noch ausgegeben wird, wann ein Lehrer in den Ruhestand geht. Dazu wird einfach 67 auf das Geburtsjahr addiert. Wir sehen, dass wir auch hier die Methode getGeburtsjahr() aufrufen müssen, da wir anders nicht an den Wert des Attributs geburtsjahr gelangen.

Geschickter wäre es, in der Klasse Person den Zugriff auf die Attribute durch Unterklassen zu erlauben. Konkret bedeutet dass, das wir in Person die Attribute als protected deklarieren:

public class Person {

  // Attribute
  protected String name;
  protected int geburtsjahr;
  
  // Konstruktor
  public Person(String pName, int pGeburtsjahr){
    name = pName;
    geburtsjahr = pGeburtsjahr;
  }

  // get-Methoden
  public String getName(){
    return name;
  }

  public int getGeburtsjahr(){
    return geburtsjahr;
  }
  
  // Ausgabe in Konsole
  public void schreibeInfo(){
    System.out.println("Mein Name ist "+name+".");
    System.out.println("Ich bin im Jahr "+geburtsjahr+" geboren.");
  }

}

Jetzt können wir in Lehrer auf die Attribute zugreifen. Achte hier genau auf die Änderung in schreibeInfo():

public class Lehrer extends Person {

  // Attribute
  private String erstesFach;
  private String zweitesFach;

  // Konstruktor
  public Lehrer(String pName, int pGeburtsjahr, String pErstesFach, String pZweitesFach){
    super(pName,pGeburtsjahr);
    erstesFach = pErstesFach;
    zweitesFach = pZweitesFach;
  }

  // get-Methoden
  public String getErstesFach(){
    return erstesFach;
  }

  public String getZweitesFach(){
    return zweitesFach;
  }
  
  // Ausgabe in Konsole
  public void schreibeInfo(){
    super.schreibeInfo();
    System.out.println("Ich unterrichte "+erstesFach+" und "+zweitesFach+".");
    System.out.println("Im Jahr "+(geburtsjahr+67)+ " gehe ich in den Ruhestand.");
  }

}

Wir könnten jetzt in der Klasse Lehrer auch noch Methoden ergänzen, die die Werte der Attribute von Person verändern. Allerdings wäre das im Sachzusammenhang hier nicht so sinnvoll.

Die Abstufungen der Sichtbarkeit von Attributen und Methoden sind in dieser Tabelle einmal zusammengefasst:

Modifier Zugriff / Sichtbarkeit Symbol
public Jeder hat Zugriff. +
protected Nur die Klasse selbst, Unterklassen und Klassen, die sich im gleichen Paket befinden, haben Zugriff. #
private Nur die Klasse selbst hat Zugriff.

Mehrfachvererbung ist verboten

Es ist durchaus möglich – und oft auch sehr praktisch – von einer Unterklasse eine weitere Unterklasse abzuleiten. Hier nur ein ganz einfaches Beispiel, um ein Bild davon vor Augen zu haben:

Die Unterklasse Lehrer hat selbst eine weitere Unterklasse.

In Anwendungsbeispielen werden wir nochmal darauf zurückkommen.

Die Klasse Lehrer dürfte auch viele Unterklassen neben der Klasse Schulleiter haben. Vielleicht möchten wir eigene Klassen für Grundschullehrer, Gymnasiallehrer etc. einführen. Das könnten wir ohne Probleme anstellen.

Umgekehrt ist es aber verboten, dass eine Klasse zwei Oberklassen hat! Vielleicht könnten wir denken, dass ein Schulleiter einerseits ein spezieller Lehrer ist und andererseits auch in spezieller Chef. Daher sollte Schulleiter die Oberklassen Lehrer und Chef besitzen:

Mehrfachvererbung ist in Java verboten.

Das ist in Java aber nicht möglich. Dies wurde ausgeschlossen, da man sich sonst sehr schnell in komplizierten Zusammenhängen zwischen vielen Klassen verirren kann, was in anderen Programmiersprachen gerne zu schwer zu findenden Programmierfehlern führt. Die Entwickler von Java wollten uns vor so etwas schützen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.