Javaschubla.de - Java als erste Programmiersprache

OO10 Vererbung

Vererbung ist die Eigenschaft der Objektorientierung, die ihr erst den Vorteil gegenüber früheren Ansätzen mit zusammengesetzten Datentypen bringt.

Nehmen wir die Klasse Angestellter, wie sie OO08 zuletzt aussah (nachdem wir eine private Hilfsmethode hinzugefügt hatten), fügen den Default-Konstruktor wieder hinzu (warum, wird später erklärt), und machen wir die Attribute und die Methode großKleinSchreibung protected statt private (auch das wird später erklärt).

class Angestellter
{
  protected String vorname;
  protected String nachname;
  protected int alter;
  protected int gehalt;

  Angestellter(String v, String n, int a, int g)
  {
    vorname = großKleinSchreibung(v);
    nachname = großKleinSchreibung(n);
    alter = a;
    gehalt = g;
  }

  Angestellter()
  {
  }

  protected String großKleinSchreibung(String name)
  {
    return Character.toUpperCase(name.charAt(0)) + name.substring(1).toLowerCase();
  }

  String getVorname()
  {
    return vorname;
  }

  String getNachname()
  {
    return nachname;
  }

  int getAlter()
  {
    return alter;
  }

  int getGehalt()
  {
    return gehalt;
  }

  void setGehalt(int neuesGehalt)
  {
    if (neuesGehalt < gehalt)
    {
      System.err.println("Das ist aber keine Erhöhung!");
    }
    else
    {
      gehalt = neuesGehalt;
    }
  }

  void erhöheGehalt(int erhöhung)
  {
    if (erhöhung <= 0)
    {
      System.err.println("Das ist aber keine Erhöhung!");
    }
    else
    {
      gehalt += erhöhung;
    }
  }

  void geburtstagFeiern()
  {
    alter++;
    System.out.println("Happy birthday " + vorname + " " + nachname + "!");
  }
}

Nehmen wir an, am Ende jeden Jahres wird das Gehalt aller Angestellten automatisch um 50 Euro erhöht. Fügen wir also noch folgende Methode hinzu, die dann einmal pro Jahr für jedes Angestellten-Objekt ausgeführt werden kann:

  void standardGehaltserhöhung()
  {
    gehalt += 50;
  }

Jetzt wollen wir spezielle Angestellte betrachten: Praktikanten bekommen keine automatische Gehaltserhöhung. Manager hingegen bekommen eine Gehaltserhöhung um 100 Euro. Manager habe zusätzlich ein Attribut firmenWagen (vom Typ String, wir speichern der Einfachheit halber nur den Typ, z.B. "BMW") und die dazugehörigen Methoden getFirmenWagen und setFirmenWagen.

Ansonsten unterscheiden sich Praktikant und Manager nicht von den anderen Angstellten: Sie haben auch einen Vornamen, einen Namen, ein Alter und ein Gehalt. Sie haben auch die Methoden geburtstagFeiern und erhöheGehalt - falls es doch mal eine Gehaltserhöhung für Praktikanten gibt, und für Manager sowieso. Und auch die get-Methoden für die vier Attribute und die setGehalt-Methode gibt es natürlich.

Jetzt könnten wir eine neue Klasse Praktikant und eine neue Klasse Manager schreiben, in die wir alle Attribute und Methoden von Angestellter kopieren, dann die Methode standardGehaltsErhöhung ändern und bei der Klasse Manager den firmenWagen hinzufügen. Aber das wäre sehr unpraktisch - z.B. wenn wir später eine Methode zu Angstellter hinzufügen, müssen wir die auch wieder in die Klassen Manager und Praktikant kopieren. Wenn wir einen Fehler in einer Methode in Angestellter korrigieren, müssten wir das in Manager und Praktikant wiederholen.

Aber in Java und anderen objektorientierten Sprachen gibt es eine viel elegantere Möglichkeit: Wir deklarieren Praktikant und Manager als Untertypen/Subklassen von Angestellter. Damit erben Praktikant und Manager die Attribute und Methoden von Angestellter - wir müssen sie nicht kopieren. Das geht mit dem Schlüsselwort "extends", was so viel bedeutet wie "erweitert".

Konstruktoren werden nicht vererbt, die müssen wir erneut hinzufügen - sie heißen ja auch anders.

class Praktikant extends Angestellter
{
  Praktikant(String v, String n, int a, int g)
  {
    vorname = großKleinSchreibung(v);
    nachname = großKleinSchreibung(n);
    alter = a;
    gehalt = g;
  }

  void standardGehaltserhöhung()
  {
    gehalt += 0;
  }
}
class Manager extends Angestellter
{
  String firmenWagen;

  Manager(String v, String n, int a, int g)
  {
    vorname = großKleinSchreibung(v);
    nachname = großKleinSchreibung(n);
    alter = a;
    gehalt = g;
  }

  Manager(String v, String n, int a, int g, String auto)
  {
    vorname = großKleinSchreibung(v);
    nachname = großKleinSchreibung(n);
    alter = a;
    gehalt = g;
    firmenWagen = auto;
  }

  void standardGehaltserhöhung()
  {
    gehalt += 100;
  }

  void setFirmenWagen(String auto)
  {
    firmenWagen = auto;
  }

  String getFirmenWagen()
  {
    return firmenWagen;
  }
}

Wie man sieht, können wir auch in den Klassen Praktikant und Manager auf die Attribute (z.B. gehalt) zugreifen.

In einer anderen Klasse, die diese Klassen benutzt, können wir ebenso auf die geerbten Methoden zugreifen:

class PraktikantenUndManagerVerwaltung
{
  public static void main(String[] args)
  {
    Praktikant p = new Praktikant("Moritz", "Leifheit", 22, 600);
    System.out.println(p.getVorname);
    p.standardGehaltserhöhung();
    System.out.println(p.getGehalt());

    Manager m = new Manager("Stefanos", "Gelati", 52, 5200, "Ferrari");
    m.standardGehaltserhöhung();
    System.out.println(m.getNachname());
    System.out.println(m.getGehalt());
    System.out.println(m.getFirmenWagen());
  }
}

Vererbung und Zugriffsmodifizierer

Wenn wir die vorher private Methode großKleinSchreibung nicht zu protected geändert hätten, hätten wir auf sie nicht in den Konstruktoren (oder anderen Methoden) zugreifen können. Was private ist, wird nicht geerbt!

Auf protected Methoden und Attribute können andere Klassen aus demselben Package zugreifen. Da wir die Klassen Angestellter/Praktikant/Manager einerseits und die Klassen AngestelltenVerwaltung/PraktikantenUndManagerVerwaltung andererseits im selben Package gelassen haben, macht es keinen Unterschied, ob sie protected, public oder ohne Modifizierer (Default-Zugriff = Package-Zugriff) sind. Stell dir also vor, die "...Verwaltung"s-Klassen wären in einem anderen Package oder ändere die Klassen mit dem Wissen aus OO09 tatsächlich dahingehend.

Was ist nun der Unterschied zwischen protected und "Package"-Zugriff? (Package-Zugriff gilt, wenn kein Modifizierer angegeben wird.) Die anderen Klassen im Package können doch auf sie zugreifen und die Klassen in anderen Packages nicht. - Einen Unterschied gibt es nur beim Erben (Ableiten, Extenden): Attribute und Methoden, die protected sind, werden geerbt, wenn man eine Klasse von einer Klasse aus einem anderen Package ableitet. Solche mit default (package) Zugriff werden dann nicht geerbt.

Übersicht:

private: kein Zugriff von anderen Klassen; wird nicht geerbt
default (package): Zugriff von Klassen im selben Package, nicht von Klassen außerhalb; wird von Klassen im selben Package geerbt, von Klassen in anderen Packages nicht
protected: Zugriff von Klassen im selben Package, nicht von Klassen außerhalb; wird geerbt
public: Zugriff von allen anderen Klassen; wird geerbt

Den Superkonstruktor aufrufen

Konstruktoren werden wie gesagt nicht geerbt. Man braucht sie aber nicht zu kopieren und umzubenennen, wie wir es oben gemacht haben, sondern kann den Konstruktor der Oberklasse/Superklasse aufrufen. Das Schlüsselwort dafür ist "super".

Statt

  Praktikant(String v, String n, int a, int g)
  {
    vorname = großKleinSchreibung(v);
    nachname = großKleinSchreibung(n);
    alter = a;
    gehalt = g;
  }

schreibt man

  Praktikant(String v, String n, int a, int g)
  {
    super(v, n, a, g);
  }

Damit wird der Konstruktor Angestellter(String v, String n, int a, int g) aufgerufen.

Der zweite Konstruktor in Manager sieht dann so aus:

  Manager(String v, String n, int a, int g, String auto)
  {
    super(v, n, a, g);
    firmenWagen = auto;
  }

Der Aufruf des Superkonstruktors muss die erste Anweisung im Konstruktor sein.

Wenn man keinen Superkonstruktor explizit aufruft, fügt der Compiler den Aufruf des Default-Superkonstruktors, also "super();" als erste Anweisung in den Konstruktor ein. Wenn die Superklasse gar keinen Defaultkonstruktor (parameterlosen Konstruktor) enthält, gibt es also einen Compilerfehler! Deshalb haben wir oben den Defaultkonstruktor wieder in der Klasse Angestellter eingefügt. Wir können ihn nun wieder löschen, da wir ja den Superkonstruktor explizit mit "super(v, n, a, g);" aufrufen.

Einen anderen Konstruktor derselben Klasse kann man übrigens mit this(Parameter) aufrufen. Z.B. könnte der zweite Manager-Konstruktor auch so aussehen:

  Manager(String v, String n, int a, int g, String auto)
  {
    this(v, n, a, g);
    firmenWagen = auto;
  }

Das macht hier keinen Unterschied, da im ersten Konstruktor Manager(String v, String n, int a, int g) ja nichts anderes gemacht wird, als super(v, n, a, g) aufzurufen.

Wenn ein anderer Konstruktor mit this() aufgerufen wird, fügt der Compiler keinen impliziten Aufruf von super(); ein .

Die Methode großKleinSchreibung kann man jetzt auch wieder private machen, wenn man will - wir hatten sie ja nur von den Konstruktoren aus aufgerufen und benötigen sie in Praktikant und Manager nun nicht mehr.

Das Substitutionsprinzip

Dass wir Praktikant und Manager von Angestellter abgeleitet haben, hab nicht nur den Vorteil, dass wir die Attribute und Methoden nicht von Angestellter in die Klassen kopieren mussten. Der entscheidende Vorteil ist, dass jedes Praktikanten-Objekt auch ein Angestellten-Objekt ist, und ebenso jedes Manager-Objekt ein Angestellten-Objekt. Das nennt man das Substitutionsprinzip (Ersetzungsprinzip). Wir können also z.B. Praktikanten, Manager und sonstige Angestellte alle in einem Angestellten-Array speichern und auf die gleiche Weise behandeln.

class JahresGehaltserhöhung
{
  public static void main(String[] args)
  {
    Angestellter[] angestellte = new Angestellter[3];
    angestellte[0] = new Angestellter("Petra"; "Müller", 45, 1800);
    angestellte[1] = new Praktikant("Moritz", "Leifheit", 22, 600);
    angestellte[2] = new Manager("Stefanos", "Gelati", 52, 5200, "Ferrari");

    for (int i = 0; i<array.length; i++)
    {
      angestellte[i].standardGehaltserhöhung();
    }
  }
}

Auch die Praktikanten und Manager werden hier nur als Angestellte behandelt - dennoch wird das Gehalt der Manager um 100 und das Gehalt der Praktikanten gar nicht erhöht.

Die Methode standardGehaltserhöhung wurde in den Klassen Praktikant und Manager "überschrieben". Überschreiben ist eine Art von "Polymorphie" ("Vielförmigkeit"). Die andere von Polymorphie ist das "Überladen" (mehrere Methoden mit dem gleichen Namen, aber unterschiedlichen Parametern).

Der statische und der dynamische Typ eines Objekts

Das kommt bereits in dem Beispiel JahresGehaltserhöhung vor, aber ich will es noch einmal deutlicher machen.

    Angestellter a1 = new Angestellter("Petra"; "Müller", 45, 1800);

Das Objekt a1 ist vom Typ Angestellter. Sowohl der statische Typ, das ist der zur Compilezeit, als auch der dynamische Typ, das ist der zur Laufzeit (Runtime), ist Angestellter.

    Angestellter a2 = new Manager("Stefanos", "Gelati", 52, 5200, "Ferrari");

Dass dies möglich ist, haben wir bereits gerade eben gesehen - wir haben Manager und Praktikanten in einem Angestellten-Array gespeichert.

Der statische Typ der Variablen a2 ist Angestellter. Der Compiler weiß nur, dass die Variable vom Typ Angestellter ist. Man kann also nur auf Attribute und Methoden zugreifen, die in der Klasse Angestellter definiert sind, nicht auf Attribute und Methoden, die erst in der Klasse Manager hinzugekommen sind. a2.getVorname() geht, a2.getFirmenWagen() geht nicht.

Der dynamische Typ des Objekts a2 ist dennoch Manager. Wenn man a2.standardGehaltserhöhung() aufruft, wird das Gehalt um 100 erhöht, wie es in der Klasse Manager festgelegt ist.

Das nennt man "Dynamic Dispatch". Zur Laufzeit (dynamisch) wird die richtige Methode, die der Klasse Manager statt die der Klasse Angestellter aufgerufen.

Wenn eine Methode in der Subklasse nicht überschrieben wurde, z.B. geburtstagFeiern, wird so oder so die geburtstagFeiern-Methode aus der Klasse Angestellter aufgerufen - Manager erbt sie ja unverändert.

    Angestellter a = new Angestellter(...);
    a.geburtstagFeiern();

oder

    Manager m = new Manager(...);
    m.geburtstagFeiern();

oder

    Angestellter ma = new Manager(...);
    ma.geburtstagFeiern();

macht also überhaupt keinen Unterschied.

Ein Objekt als Parameter an eine Methode übergeben

Wenn ein Objekt als Parameter an eine Methode übergeben wird, die überladen ist, von der es also mehrere Versionen mit verschiedenen Parametern gibt, dann ist der statische Typ ausschlaggebend!

class Statisch
{
  public static void main(String[] args)
  {
    Angestellter stef = new Manager("Stefanos", "Gelati", 52, 5200, "Ferrari");
    meth(stef); // gibt "Der Angestellte ..." aus
  }

  static void meth(Angestellter a)
  {
    System.out.println("Der Angestellte "+a.getVorname()+" "+a.getName()+" ("+a.getAlter()+") verdient "
                       + a.getGehalt()+".");
  }

  static void meth(Manager m)
  {
    System.out.println("Der Manager "+m.getVorname()+" "+m.getName()+" ("+m.getAlter()+") verdient "
                       + m.getGehalt()+"und fährt einen "+m.getFirmenWagen()+".");
  }
}

Die Wurzel des Vererbungsbaumes

Alle Klassen erben von der Klasse Object (java.lang.Object). Auch wenn man nicht "extends Object" hinschreibt, erben sie trotzdem von Object alle Methoden (sie hat keine public oder protected Attribute, die geerbt werden könnten, sonst würden auch diese geerbt werden). Dazu gehört z.B. die Methode equals.

Zur Erinnerung: Auch Arrays sind Objekte und erben alle Methoden von java.lang.Object.

Keine Mehrfachvererbung

Eine Klasse kann nur eine andere Klasse extenden, nicht mehrere.

Wenn eine Klasse zwei Klassen A und B extenden könnte, und beide Klassen hätten ein Attribut oder eine Methode mit demselben Namen, wäre sonst nicht klar, welche von beiden geerbt würde. (Es gibt aber andere objektorientierte Sprachen wie C++, die Mehrfachvererbung erlauben. Das hat so viele Probleme bereitet, dass die Erfinder von Java das in Java nicht übernommen haben.)

Eine konfliktfreie Art von Mehrfachvererbung wird durch Interfaces realisiert, die wir als nächstes kennen lernen werden.