04.05 Vererbung

instanceof und Cast

Wir ändern nun das vorherige Beispiel etwas ab. Wir belassen alle drei Klassen Person, Manager und Programmierer so wie sie sind und ändern nur die main Methode.

public static void main(String[] args) {
		
  Person personen[] = new Person[3];
  String s = "";
		
  personen[0] = new Person("Horst");
  personen[1] = new Programmierer("Sebastian", "Java");
  personen[2] = new Manager("Wendelin", 50000000);
		
  for (int i = 0; i < personen.length; i++) {
			
    s = personen[i].getName();
    if (personen[i] instanceof Manager) {
      s += " " + ((Manager)personen[i]).getGehalt();
    } 
    else if (personen[i] instanceof Programmierer) {
      s += "  " + ((Programmierer)personen[i]).getLieblingsSprache();
    }
			
    System.out.println(s);
  }
}

An Stelle der drei einzelnen Objekte möchten wir nun alle Objekte in ein Feld verpacken. Dazu deklarieren wir ein Array personen, welches Elemente von Typ Person aufnehmen kann.
Das erste Element ist wieder unser „Horst“. Als zweites Element soll nun ein Objekt der Klasse Programmierer eingefügt werden. Das Array kann doch aber nur Elemente des Typs Person enthalten, werden Sie sich jetzt sicher fragen. Hierzu soll noch einmal folgendes wiederholt werden. Da die Klasse Programmierer eine Kindklasse von Person ist, gilt wie bereits erwähnt: Jeder Programmierer ist eine Person. Somit ist es auch kein Problem ein Objekt der Klasse Programmierer in dieses Array einzufügen. Gleiches gilt auch wieder für unseren Manager „Wendelin“. Nun möchten wir aber wieder so eine schöne Ausgabe haben wie in unserem ersten Beispiel zur Vererbung. Das gestaltet sich nun nicht mehr ganz so einfach wie zuvor. Wir haben jetzt ein Feld, dessen Elemente alle Objekte der Klasse Person sind. Wenn wir jetzt schreiben würden personen[i].getGehalt() würde dies nicht funktionieren. Die Klasse Person kennt nämlich keine Methode namens getGehalt. Diese ist ja nur in unserer Klasse Manager implementiert. Es muss also nun eine Möglichkeit geben, die im Array enthaltenen Objekte dahin gehend zu überprüfen, zu welcher Klasse sie denn gehören. Für diesen Zweck gibt es in Java den instanceof Operator.

if (meinObjekt instanceof MeineKlasse) {}

Hiermit kann man überprüfen, ob meinObjekt eine Instanz von MeineKlasse ist. In unserem Fall überprüfen wir also die Elemente des Arrays dahingehend, ob es Instanzen der Klasse Manager bzw. Programmierer sind. Damit haben wir aber erst einmal nur die Hälfte geschafft.
Wir wissen zwar nun, dass beispielsweise unser aktuelles Feldelement ein Objekt der Klasse Manager ist, aber dennoch ist jedes Element des Arrays immer noch vom Typ Person. Es muss nun eine Möglichkeit geben ein Objekt, von dem wir nun wissen von welcher Klasse es ist, auch wieder in Objekt dieser Klasse umzuwandeln. Dies ist in Java recht einfach möglich.

(KonkreteKlasse)objekt

Man schreibt einfach vor das Objekt in runden Klammern die Klasse, in welche wir das dahinter stehende Objekt gerne umwandeln möchten. Man spricht hierbei von einem Cast (eng. Abguss). In unserem Fall bedeutet dies, Objekte der Klasse Person müssen in Objekte der Klassen Manager bzw. Programmierer umgewandelt werden. Dies geschieht durch ((Manager)personen[i]) bzw.((Programmierer)personen[i]). Innerhalb der Klammern wandeln wir nun ein Objekt vom Typ Person in das entsprechende konkretere Objekt um. Somit können wir dann auch die jeweiligen Methoden der konkreten Klassen, wie getGehalt und getLieblingsSprache, verwenden. Als Ausgabe erhalten wir.

Horst
Sebastian  Java
Wendelin 50000000

Vererbung und static

Interessant ist noch das Verhalten statischer Elemente bei der Vererbung. Hier kann es häufig zu Fehlern kommen, da man mitunter die Wirkungsweise bzw. die interne Abarbeitung nicht berücksichtigt. Hierzu ein kleines Beispiel.

public class Vater {

  protected static int classAttr = 0;
  protected int objectAttr = 0;
	
  public static void setClassAttr(int aNewValue) {
		
    Vater.classAttr = aNewValue;
  }
	
  public static int getClassAttr() {
		
    return Vater.classAttr;
  }
	
  public void setObjectAttr(int aNewValue) {
		
    this.objectAttr = aNewValue;
  }
	
  public int getObjectAttr() {
		
    return this.objectAttr;
  }
}

Dies ist unsere Klasse von der später eine andere Klasse erben soll. Sie enthält jeweils ein Objekt- und ein Klassenattribut, welche mit den zugehörigen Set- bzw. Get-Methoden gesetzt bzw. abgefragt werden können. Nun lassen wir eine Klasse Kind von der Klasse Vater erben.

public class Kind extends Vater {

  public static void setClassAttr(int aNewValue) {

    Kind.classAttr = aNewValue;
  }

  public static int getClassAttr() {

    return Kind.classAttr;
  }

  public void setObjectAttr(int aNewValue) {

    this.objectAttr = aNewValue;
  }

  public int getObjectAttr() {

    return this.objectAttr;
  }
}

Nun testen wir das Verhalten beider Klassen.

public static void main(String[] args) {

  Vater.setClassAttr(5);
  Kind.setClassAttr(3);
		
  Vater vater = new Vater();
  vater.setObjectAttr(6);
  Kind kind = new Kind();
  kind.setObjectAttr(4);

  System.out.println(Vater.getClassAttr());
  System.out.println(Kind.getClassAttr());
		
  System.out.println(vater.getObjectAttr());
  System.out.println(kind.getObjectAttr());
}

Wir erhalten überraschend folgende Ausgabe:

3
3
6
4

Für die Ausgabe der Klassenattribute erhalten wir für Kind und Vater jeweils 3 und für die Objektattribute 4 und 6. Dies hat folgenden Hintergrund. Das Klassenattribut classAttr existiert unabhängig von einer konkreten Instanz der Klasse Vater bzw. eines Erben davon. Es ist somit gesehen auch nur einmal vorhanden. Innerhalb der Methode public static void setClassAttr(int aNewValue) der Klasse Kind steht zwar Kind.classAttr = aNewValue; dennoch ändert diese Zuweisung das Klassenattribut der Vaterklasse. Somit wird das Attribut classAttr der Klasse Vater innerhalb der Main-Methode erst auf 5 (Vater.setClassAttr(5);) und danach sofort auf 3 (Kind.setClassAttr(3);) gesetzt. Anders verhält es sich bei dem Objektattribut objectAttr. Dieses Attribut ist immer an eine konkrete Instanz gebunden und existiert somit auch für jede einzelne Instanz. Die Methoden public void setObjectAttr(int aNewValue) und public int getObjectAttr() der Klasse Vater bzw. Kind arbeiten demnach auch immer mit dem zur Instanz zugehörigen Attribut. Als Ausgabe erhalten wir deswegen auch richtig 6 und 4.

Man kann noch hinzufügen, dass man in der Klasse Kind auch alle Methoden weglassen könnte. Die statischen Methoden arbeiten mit dem Klassenattribut classAttr der Vaterklasse und die Objektmethoden überschreiben auch nur die Methoden der Vaterklasse bei Verwendung der gleichen Logik. Somit würde ein

public class Kind extends Vater {}

ebenfalls zum gleichen Ergebnis führen.

Im Kapitel 04.06. Polymorphie erfahren Sie mehr dazu, wie man vererbte Methoden überschreiben kann, um ein vielgestaltiges Verhalten zu realisieren.

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.