04.05 Vererbung

Ein wichtiger Bestandteil bei der objektorientierten Programmierung ist das Konzept der Vererbung. Hiermit ist es möglich Eigenschaften einer Klasse an seine Nachkommen weiter zugeben. Die Struktur, welche durch Vererbung entsteht, nennt man Vererbungshierarchie.

Vererbung

Java selbst ist komplett als Vererbungshierarchie aufgebaut. Alle in Java vorkommenden Klassen sind direkte bzw. indirekte Nachfahren der Klasse Object aus dem Paket java.lang. Eine solche Klasse wird Elternklasse, Superklasse, Basisklasse oder Oberklasse genannt. Die davon abgeleiteten Klassen nennt man Kindklasse, Subklasse oder Unterklasse. Der Vererbungsvorgang selbst ist eine gerichtete Beziehung zwischen der Eltern- und Kindklasse. Man spricht hierbei von Generalisierung.

Die Elternklasse ist eine Generalisierung der Kindklasse.
Die Kindklasse ist eine Spezialisierung der Elternklasse.

Man kann somit sagen, ein Objekt der Kindklasse ist ein Objekt der Elternklasse.
Dies gilt allerdings nicht in die Gegenrichtung. Ein Objekt der Elternklasse ist kein Objekt der Kindklasse. Der Grund hierfür liegt darin, dass eine Kindklasse eine speziellere Ausprägung der Elternklasse ist. Das heißt, dass diese alle Eigenschaften der Elternklasse besitzt und zusätzlich noch (speziellere) Eigenschaften. Somit erfüllt eine Kindklasse immer die Eigenschaften der Elternklasse, aber eine Elternklasse niemals die der (spezielleren) Kindklasse. Nach diesem ganzen Schwall an Terminologie schauen wir uns einmal so eine Vererbung an. Dazu bietet sich Verwendung der UML Notation an.

Bei der UML Notation der Vererbung werden beide Klassen mit einer gerichteten durchgehenden Linie verbunden. Der geschlossene, nicht ausgefüllte Pfeil zeigt immer auf die Elternklasse. Hieran kann man nun ablesen, dass die Klasse Kindklasse ein Nachfahre der Klasse Elternklasse ist.

In Java

Da wir uns nun schon etwas in der Begriffswelt der Vererbung und der zugehörigen UML Notation auskennen, schauen wir uns nun die Umsetzung in Java an. Für Vererbungsbeziehungen unter Klassen existiert das Schlüsselwort extends (deutsch: erweitert).

public class Kindklasse extends Elternklasse {}

Umgangssprachlich ausgedrückt erweitert also eine Kindklasse die Elternklasse.
Die Kindklasse erbt somit alle sichtbaren Eigenschaften der Elternklasse und kann zusätzlich noch weitere (speziellere) Eigenschaften implementieren. Dies testen wir jetzt einfach mal an einen Beispiel.

Dies soll einmal unsere Vererbungshierarchie darstellen. Es gibt eine Klasse Person, welche die Elternklasse repräsentiert. Zusätzlich gibt es noch die Klassen Programmierer und Manager, welche Kindklassen von Person sein sollen. Hieran kann man auch wieder die Beziehungen untereinander darstellen, example pharmaci….ialis.

Ein Manager ist eine Person. Ein Programmierer ist eine Person.
Aber
Nicht jede Person ist ein Manager. Nicht jede Person ist ein Programmierer.

Wir wollen nun versuchen das obige UML Diagramm zu lesen. Wir haben also eine Klasse Person. Diese enthält ein Attribut name. Der Konstruktor dieser Klasse erwartet einen String Parameter aName, um den Namen der Person direkt bei der Erzeugung mit zu übergeben. Weiterhin gibt es eine Methode getName, welche uns den Namen der Person zurückliefern soll. Die Klasse Manager ist ein Kind der Klasse Person. Dies erkannt man an dem gerichteten Pfeil. Aus diesem Grund „erbt“ die Klasse Manager bereits das Attribut name sowie die Methode getName. Zusätzlich dazu implementiert sie noch ein Attribut namens gehalt mit der zugehörigen Methode getGehalt. Analog verhält sich dies ebenso bei der Klasse Programmierer, mit dem Unterschied, dass hier an Stelle von gehalt die lieblingsSprache als Attribut vorkommt. Beginnen wir nun mit der Implementierung der Klasse Person.

public class Person {
	
  private String name = "";
	
  public Person(String aName) {
    this.name = aName;
  }
	
  public String getName() {
    return this.name;
  }
}

Dies kennen wir ja bereits alles. Eine normale Klasse, welche ein Attribut, einen Konstruktor und eine Methode enthält. Schauen wir uns nun die Klasse Manager an.

public class Manager extends Person {
	
  private int gehalt;
	
  public Manager(String aName, int aGehalt) {
		
    super(aName);
    this.gehalt = aGehalt;
  }

  public int getGehalt() {
    return this.gehalt;
  }
}

Diese Klasse enthält zusätzlich – zu dem bisher Bekannten – das Schlüsselwort extends gefolgt von der Klasse, von welcher sie erben soll. In diesem Fall also Person. Auch im Konstruktor von Manager gibt es etwas Neues zu sehen. Hier rufen wir über super den Konstruktor der Elternklasse auf und übergeben diesem den Parameter aName.

Mit dem Schlüsselwort super wird immer die Elternklasse angesprochen.

Der Rest ist wieder bekannt. Die Klasse Programmierer sieht wieder so ähnlich aus.

public class Programmierer extends Person {

  private String lieblingsSprache = "";
	
  public Programmierer(String aName, String aSprache) {
		
    super(aName);
    this.lieblingsSprache = aSprache;
  }
	
  public String getLieblingsSprache() {
    return this.lieblingsSprache;
  }
}

Sie erbt von der Klasse Person und verwendet in ihrem eigenen Konstruktor den Konstruktor ihrer Elternklasse. Das Ganze testen wir nun innerhalb einer main Methode.

public static void main(String[] args) {
		
  Person horst = new Person("Horst");
  Programmierer sebastian = new Programmierer("Sebastian", "Java");
  Manager wendelin = new Manager("Wendelin", 50000000);
		
  System.out.println(horst.getName());
  System.out.println(sebastian.getName() + " " + sebastian.getLieblingsSprache());
  System.out.println(wendelin.getName() + " " + wendelin.getGehalt());
}

Hier erzeugen wir ein Objekt der Klasse Person mit dem Namen horst. Dem Konstruktor von Person geben wir auch gleich den Namen „Horst“ mit. Danach erzeugen wir ein Objekt der Klasse Programmierer mit dem Namen sebastian. Der Konstruktor hierzu bekommt „Sebastian“ und „Java“ als Parameter übergeben. Als drittes und letztes Objekt folgt wendelin der Klasse Manager mit den Parametern „Wendelin“ und „50000000“ für den zugehörigen Konstruktor. Zum Schluss lassen wir uns nun noch einige Werte der Objekte ausgeben. Mit horst.getName() erhalten wir Horst als Rückgabe, da getName eine Methode der Klasse Person ist. Wir erhalten aber auch über sebastian.getName() den entsprechenden Namen Sebastian. Dies geschieht deshalb, da die Klasse Programmierer, wovon sebastian ein Objekt ist, die Methode getName von der Klasse Person erbt. Gleiches gilt ebenso für unser Objekt wendelin der Klasse Manager. Als Ausgabe erhalten wir.

Horst
Sebastian Java
Wendelin 50000000

Auf der nächsten Seite erfahren Sie einige Details, um zu bestimmen zu welcher Klasse ein Objekt gehört und wie man es in ein anderes Objekt umwandeln kann.

5 Replies to “04.05 Vererbung”

  1. Cyrill Brunner

    Momentan arbeite ich intensiv mit dem src-Code von Minecraft. Näheres Wissen ist nicht erforderlich, jedoch habe ich einige Fragen, die man auch mit normalen Java-Kenntnissen beantworten können sollte, und zwar:
    1. Wie ist es möglich, von einer Kindklasse explizit einen Konstruktor zu verlangen, der den „super-Konstruktor“ aufrufen muss?
    2. Wie ist es möglich, einzustellen, dass vor dem Objektnamen nur die Superklasse angegeben werden darf? Wenn man nämlich die Kindklasse angibt, wird explizit nach einem cast verlangt.

    Ich hoffe, das ganze ist auch ohne Beispiele verständlich.

  2. Stefan Kiesel

    Hallo Cyrill Brunner,

    die Kind-Klasse muss immer einen Konstruktor aufrufen. Im Zweifelsfall den Default-Konstruktor ohne Parameter (sofern vorhanden). Dieser wird auch dann ausgeführt, wenn nichts anderes angegeben ist. Vorschreiben, dass explizit ein Konstruktor aufgerufen werden muss, kann man nicht. Es sei denn man bietet nur diesen einen Konstruktor in der Vaterklasse an.
    Die 2. Frage verstehe ich leider nicht, sorry! Was bedeutet „vor dem Objektnamen“? Wo soll etwas „eingestellt“ werden?

    Grüße
    Stefan

  3. Cyrill Brunner

    Das mit dem ersten habe ich jetzt verstanden.
    Zum 2.:
    Mit dem Objektnamen meine ich den Namen der Instanz, vor der ja immer entweder die Klasse oder der primitive Dateityp angegeben wird.
    Zum Beispiel funktioniert
    public static final Block Flower1 = new BlockFlower(100, 4).set…
    , dies jedoch nicht:
    public static final BlockFlower Flower2 = new BlockFlower(101, 5).set…
    , da wird nach dem = explizit der cast (BlockFlower) verlangt. Hinzuzufügen ist, dass BlockFlower eine Kindklasse von Block ist.
    Ich hoffe, ich konnte das ganze verständlich formulieren.

    Grüsse
    Cyrill

  4. Stefan Kiesel

    Ja, ich denke das Problem ist jetzt klar. Ich nehme an

    new BlockFlower(100, 4).set...

    erzeugt eine Instanz von BlockFlower, hat aber als Rückgabewert Block?!

    Da BlockFlower eine Kindklasse von Block ist, kommt man hier nicht um einen Cast herum. Würde die set-Methode von BlockFlower hingegen als Return-Typ BlockFlower haben, könnte man problemlos beide Varianten (Block Flower1 und BlockFlower Flower2) verwenden. Warum? Eigentlich ist es ganz einfach, ich versuche es an einem anschaulichen Beispiel zu erklären:

    Angenommen es gibt die Klassen Mensch (mit den Attributen Name und Geburtstag) und Angestellter (mit dem zusätzlichen Attribut Gehalt). Angestellter erbt natürlich von Mensch (Angestellter ist ein Mensch), so wie BlockFlower von Block erbt (BlockFlower ist ein Block). Wenn man jetzt eine Methode erzeugeMensch hat, die einen Menschen zurückliefert, dann kann man ja nicht einfach schreiben

    Angestellter a = Mensch.erzeugeMensch();

    Denn ein Mensch ist kein Angestellter und hat somit auch kein Gehalt. Das Gehalt wäre also undefiniert. Umgekehrt würde natürlich

    Angestellter a = Angestellter.erzeugeAngestellten();
    Mensch m = Angestellter.erzeugeAngestellten();

    funktionieren, da ein Angestellter ein Angestellter ist (Fall 1, problemlos möglich) und ein Angestellter eben auch ein Mensch ist (Fall 2). Denn ein Angestellter hat alle Attribute (und noch mehr), die auch ein Mensch hat. Der Mensch wäre also wohl definiert.

    Würde jetzt die Methode erzeugeMensch() intern einen Angestellten erzeugen, also bspw. sowas:

    public static Mensch erzeugeMensch() {
      return new Angestellter();
    }

    dann könnte man den Cast von Mensch nach Angestellter durchführen. Java kann aber nicht garantieren, dass es sich um einen Angestellten handelt, weil der Return-Typ eben als Mensch deklariert wurde. Das macht den Cast notwendig.

    Ich hoffe der Sachverhalt ist jetzt klarer?!

    Grüße
    Stefan

  5. Cyrill Brunner

    Das mit dem Rückgabewert von Block ist das Problem, denn in der Klasse sind alle Setter mit dem Rückgabewert des Objektes bezeichnet, um das Method-chaining zu ermöglichen. Danke für die ausführliche Antwort.

    Gruss
    Cyrill

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.