Letztes Update: 10.04.2018
Behandelte Befehle: this, this(), instanceof, Casten, Object, Überschreiben (override)

Lernziele

  • Konstruktoren mit this() und super() verschalten
  • Erkennen, ob ein statischer oder dynamischer Typ vorliegt und mit instanceof und Casting arbeiten
  • Gezielt Methoden überschreiben, so dass im Kontext die korrekte Methode aufgerufen wird
  • Abstrakte Klassen und Methoden einsetzen, um eine Klassenhierarchie effizienter und gleichzeitig robust zu gestalten

Wir bleiben bei der Theorie und sehen uns an, wie Konstruktoren genau funktionieren und was es mit Polymorphie (Vielgestaltigkeit) auf sich hat.

Wir sehen außerdem, wie man mit abstrakten Klassen und Methoden hantiert, um den Code robuster gegen falsche Verwendung zu machen.

19.1 Punktnotation

Vorab noch eine kurze Wiederholung der Punktnotation in Form eines Videos. Bitte stellen Sie sicher, dass Sie die Punktnotation beherrschen.

Video: Punktnotation (7:03)

Zusammenfassung

Bei Punktnotation wertet Processing/Java von links nach rechts aus. Sobald ein Punkt vorkommt, wird die Methode ausgeführt und der Rückgabewert eingesetzt. Im Fall von Variablen wird der Variableninhalt eingesetzt. Dann wird der nächste Punkt ausgewertet, sofern vorhanden.

19.2 this und this()

Video: Konstruktoren und "this" (9:44)

Hier geht es zunächst um das Schlüsselwort this . Doch Vorsicht! - genauso wie bei keyPressed oder mousePressed, gibt es this in zwei Varianten:

  • als Variable this
  • als Funktion this()

Variable this

Die Variable this ist ein Zeiger eines Objekts auf sich selbst. Wozu man das braucht? Sie kennen sicher das folgende Problem beim Schreiben eines Konstruktors:

public class Foo {

  // Instanzvariablen
  private String label;
  private int num;

  // Parameter und Instanzvariablen heißen gleich
  public Foo(String label, int num) {

    label = label; // Fehler!
    num = num;     // Fehler!

  }
}

Sie würden die Parameter für den Konstruktor gern genauso nennen wie die Instanzvariablen. Warum auch nicht? Warum sich einen zweiten Namen für jede Variable ausdenken? Problem ist nur, dass Processing dann nicht unterscheiden kann, welche Variable wo gemeint ist, wenn dort "label = label" steht. In diesem Fall muss Processing den Konstruktorparameter label nehmen und zwar für beide Seiten, so dass der Wert der Variable sich nicht ändern (selbst wenn: es ist eh die falsche, wir wollen schließlich die Instanzvariable setzen).

Die Lösung liefert this . Denn dank Punktnotation kann man schreiben this.label und es ist klar, dass hier die Instanzvariable gemeint ist.

public class Foo {

  // Instanzvariablen
  private String label;
  private int num;

  // Parameter und Instanzvariablen heißen gleich
  public Foo(String label, int num) {

    this.label = label; // Instanzvariable = Parameter
    this.num = num;     // Instanzvariable = Parameter

  }
}

Funktion this()

Die Funktion this() hat eine gänzlich andere Bedeutung. Die Funktion ist nämlich ein Konstruktoraufruf der eigenen Klasse, ähnlich wie super() den Konstruktor der Oberklasse aufruft.

Warum will man den eigenen Konstruktor aufrufen? Das tut man dann, wenn man mehrere Konstruktoren hat und miteinander verschränken will, um Code-Duplizierung zu vermeiden.

Nehmen wir an, Sie wollen für die Klasse Person einen "Basiskonstruktor" anbieten. Die Eigenschaften Alter und Größe werden hier automatisch auf zwei Grundwerte gesetzt.

public class Person {
  private String vorname;
  private String nachname;
  private int alter = 20;
  private float groesse = 1.80;

  public Person(String vorname, String nachname) {
    this.vorname = vorname;
    this.nachname = nachname;
  }
}

Jetzt erweitern Sie die Klasse um verschiedene "Komfort-Konstruktoren", wo Sie wahlweise auch Alter und Größe übergeben können:

public class Person {
  private String vorname;
  private String nachname;
  private int alter = 20;
  private float groesse = 1.80;

  public Person(String vorname, String nachname) {
    this.vorname = vorname;
    this.nachname = nachname;
  }

  public Person(String vorname, String nachname, int alter) {
    this.vorname = vorname;
    this.nachname = nachname;
    this.alter = alter;
  }

  public Person(String vorname, String nachname, float groesse) {
    this.vorname = vorname;
    this.nachname = nachname;
    this.groesse = groesse;
  }
}

Beachten Sie, dass Sie hier Code-Duplizierung vorliegen haben. In allen Konstruktoren führen Sie folgenden Code aus:

this.vorname = vorname;
this.nachname = nachname;

Warum könnte das problematisch sein? Vielleicht dann, wenn Sie die übergebenen Namen in irgendeiner Form verarbeiten, bevor Sie sie in den Instanzvariablen ablegen.

In Frankreich werden die Nachnamen oft in Großbuchstaben geschrieben, so dass Sie in einer späteren Version den Nachnamen mit toUpperCase direkt im Konstruktor ändern wollen:

public class Person {
  private String vorname;
  private String nachname;
  private int alter = 20;
  private float groesse = 1.80;

  public Person(String vorname, String nachname) {
    this.vorname = vorname;
    this.nachname = nachname.toUpperCase();
  }

  public Person(String vorname, String nachname, int alter) {
    this.vorname = vorname;
    this.nachname = nachname.toUpperCase();
    this.alter = alter;
  }

  public Person(String vorname, String nachname, float groesse) {
    this.vorname = vorname;
    this.nachname = nachname; // Ups, vergessen!
    this.groesse = groesse;
  }
}

Wie Sie sehen, können einem bei Code-Duplizierung Fehler unterlaufen. Stattdessen benutzen wir this(), um den Basiskonstruktor von den anderen Konstruktoren aufzurufen:

public class Person {
  private String vorname;
  private String nachname;
  private int alter = 20;
  private float groesse = 1.80;

  public Person(String vorname, String nachname) {
    this.vorname = vorname;
    this.nachname = nachname.toUpperCase();
  }

  public Person(String vorname, String nachname, int alter) {
    this(vorname, nachname);
    this.alter = alter;
  }

  public Person(String vorname, String nachname, float groesse) {
    this(vorname, nachname);
    this.groesse = groesse;
  }
}

Sie sehen, dass das ähnlich funktioniert wie super(), nur dass hier der Konstruktor in der eigenen Klasse (nicht in der Oberklasse) gesucht wird. In unserem Fall sucht Processing bei this(vorname, nachname) nach einem Konstruktor mit zwei String-Parametern; das ist offensichtlich der erste. Dort werden die Namen gesetzt und auch das toUpperCase durchgeführt, also an einer einzigen Stelle.

Zusammenfassung

  • Die Variable this zeigt auf das eigene Objekt, in der sich dieser Aufruf befindet. Wenn Sie sich in der Klasse Foo befinden und eine Instanzvariable namens x haben, können Sie mit this.x ausdrücken, dass die Instanzvariable dieser Klasse gemeint ist. Dies wird häufig im Konstruktor verwendet, um die Instanzvariable von einem gleichnamigen Parameter zu unterscheiden. Sie können auf gleiche Weise Methoden aufrufen, zum Beispiel: this.doSomething(0.25, "boo").
  • Die Funktion this() ruft den Konstruktor der eigenen Klasse auf.
    • Die Parameterliste des Aufrufs bestimmt, welcher Konstruktor gewählt wird.
    • Der this()-Aufruf darf nur in einem Konstruktor erfolgen und muss die erste Anweisung sein.
    • wird z.B. dazu verwendet, um einen allgemeinen "Basiskonstruktor" zu schreiben, auf den andere Konstruktoren zurückgreifen.

Übungsaufgaben

(a) Anrede 1

Schreiben Sie eine Klasse Kunde mit der einzigen Instanzvariablen "name" (String).

Die Klasse soll drei Konstruktoren haben: (a) mit einem Namen, (b) mit Vornamen und Nachnamen, (c) mit Vornamen, Nachnamen, Titel. Der Konstruktor soll die Namen zu einenm einzigen String zusammensetzen und in "name" ablegen. Verwenden Sie unbedingt this().

Testen Sie Ihren Code mit

public static void main(String[] args) {
  Kunde k1 = new Kunde("Hans Meier");
  Kunde k2 = new Kunde("Vera", "Schneider");
  Kunde k3 = new Kunde("Dr.", "Heiko", "Rechthaber");

  System.out.println(k1.name);
  System.out.println(k2.name);
  System.out.println(k3.name);
}

Sie sollten sehen:

Hans Meier
Vera Schneider
Rechthaber Dr. Heiko

(b) Anrede 2

Die Klasse Brief speichert Briefe in Objekten. Die Anrede ("Liebe Frau Huber" etc.) wird dort als String gespeichert. Der Brieftext ist hier irrelevant.

public class Brief {
  private String anrede = "Lieber Herr Nobody,"; // Nur Platzhaltertext
  private String text = "Bla bla bla bla bla bla bla bla bla.";

  public String toString() {
    return anrede + "\n\n" + text;
  }
}

Schreiben Sie Konstruktoren, in denen die Anrede "zusammengebaut" wird, d.h. die Instanzvariable anrede wird neu gesetzt. (Es sind keine neuen Instanzvariablen nötig oder erwünscht.)

Es sollen drei Konstruktoren werden, so dass man Briefe mit

  • nur dem Nachnamen oder
  • Vor- und Nachnamen oder
  • Vor-/Nachname und Titel erstellen kann.

Jeder Konstruktor bekommt außerdem einen boolschen Parameter, der das Geschlecht angibt (true = weiblich).

Überlegen Sie sich, wie Sie mit Hilfe von this() auf einen Basiskonstruktor zurückzugreifen können. Hier sehen Sie Testfälle, die dann laufen sollten:

public static void main(String[] args) {
  // Testfälle
  Brief b = new Brief("Schmidt", false);
  Brief b2 = new Brief("Vera", "Schmidt", true);
  Brief b3 = new Brief("Lea", "Kraft", "Prof. Dr.", true);

  // Ausdrucken
  System.out.println(b);
  System.out.println("---");
  System.out.println(b2);
  System.out.println("---");
  System.out.println(b3);
}

Hinweis: Sie schreiben zunächst den Basiskonstruktor und die beiden anderen Konstruktoren enthalten jeweils nur eine Zeile mit einem this()-Aufruf.

Auf Ihrer Konsole sollte erscheinen:

Lieber Herr Schmidt

Bla bla bla bla bla bla bla bla bla.
---
Liebe Frau Vera Schmidt

Bla bla bla bla bla bla bla bla bla.
---
Liebe Frau Prof. Dr. Lea Kraft

Bla bla bla bla bla bla bla bla bla.

19.3 Methoden überschreiben

Video: Methoden überschreiben (override) (5:22)

In einer Klassenhierarchie dürfen (und sollen) Unterklassen in bestimmten Fällen eine Methode der Oberklasse ersetzen. Betrachten wir folgendes konkrete Beispiel mit den drei Klassen Tier, Vogel und Katze. Klasse Tier hat die Methode sprich():

public class Tier {
  public void sprich() {
    System.out.println("grrr");
  }
}

Die Unterklassen sollen aber eigene Varianten der Methode haben, die für Ihre Klasse sinnvoller sind.

public class Vogel extends Tier {
  public void sprich() {
    System.out.println("fieeeeep");
  }
}

public class Katze extends Tier {
  public void sprich() {
    System.out.println("miauuuuuu");
  }
}

Wir können die Methoden in einer statischen main-Methode testen (z.B. innerhalb von Tier):

public static void main(String[] args) {
    Tier t = new Tier();
    Katze k = new Katze();
    Vogel v = new Vogel();

    t.sprich();
    k.sprich();
    v.sprich();
}
grrr
miauuuuuu
fieeeeep

Sie sehen, dass - obwohl Methoden ja vererbt werden - immer die "speziellere" Variante gewählt wird. Bei einer Katze wird nicht Methode spricht() der Klasse Tier gewählt, sondern eben die Methode, die in Klasse Katze definiert wird. Das entspricht sicher Ihrer Intuition. Man sagt, die Methode sprich() wird in Klasse Katze (und Vogel) überschrieben.

Im Englischen nennt man das method overriding und NICHT overwriting - to override bedeutet "außer Kraft setzen" oder "etwas überordnen".

Damit Sie eine Methode überschreiben dürfen, müssen Sie ein paar Regeln beachten:

  • die zu überschreibende Methode muss die gleiche Signatur haben
  • die zu überschreibende Methode muss den gleichen Rückgabetyp haben
  • die zu überschreibende Methode darf nicht private oder static sein

Zusammenfassung

Eine Unterklasse kann eine Methode der Oberklasse mit gleichem Namen, gleicher Parameterliste und gleichem Rückgabetyp neu definieren. Dies nennt man Überschreiben. Zur Laufzeit wird immer die speziellere Methode ausgeführt.

19.4 Casting

Video: Statischer Typ vs. dynamischer Typ (8:12)

In einer Klassenhierarchie haben Sie Typen und Subtypen. Schauen wir uns die Klasse Tier an, die Oberklasse der Klasse Vogel ist:

public class Tier {
}

public class Vogel extends Tier {
}

Dann ist folgendes korrekt:

Tier x = new Vogel(); // Ein Vogel IST EIN Tier

Umgekehrt können Sie nicht in einem Subtypen ein Objekt der Oberklasse speichern:

Vogel v = new Tier(); // Ein Tier IST NICHT immer ein Vogel

Wenn wir uns nochmal die korrekte Richtung anschauen:

Tier x = new Vogel();

Aber welchen Typ hat denn jetzt x? Ist x vom Typ Tier oder vom Typ Vogel? Wenn wir eine Methode aufrufen, müssen wir schließlich wissen, aus welcher Klasse die Methode gezogen wird.

Betrachten wir folgendes konkrete Beispiel mit den drei Klassen Tier, Vogel und Katze, die alle unterschiedliche Varianten der Methode sprich() haben:

public class Tier {
  public void sprich() {
    System.out.println("brumm");
  }
}

public class Vogel extends Tier {
  public void sprich() {
    System.out.println("fieeeeep");
  }
}

public class Katze extends Tier {
  public void sprich() {
    System.out.println("miauuuuuu");
  }
}

Was passiert jetzt im folgenden Code: Wird die Methode von Tier oder von Katze ausgeführt?

public static void main(String[] args) {
  Tier x = new Katze();
  x.sprich();
}

Wenn Sie den Code laufen lassen, sehen Sie:

miauuuuuu

Dies entspricht hoffentlich Ihrer Intuition: Es wird immer die "speziellere" Methode gewählt. Warum sollte ein Programmierer denn sonst auch die Methode sprich in Katze implementiert haben? Im nächsten Abschnitt erfahren wir, auf welcher Regel dieses Verhalten basiert.

Statischer und dynamischer Typ

Die Anwort auf obige Frage ist: der statische Typ ist Tier. Das ist also der Typ, den man in der Variablendeklaration angibt. Der dynamische Typ ist dagegen Vogel, das ist der Typ, den das im Beispiel erzeugte Objekt eigentlich besitzt.

Tier x = new Vogel(); // statisch: Tier, dynamisch: Vogel

Der dynamische Typ ist also immer entweder gleich dem statischen Typ oder ein Subtyp des statischen Typs.

Warum nennt man das statisch und dynamisch? Nachdem Sie eine Variable definiert haben, hat diese Variable immer den gleichen Typ, der Variablentyp bleibt also statisch. Dagegen kann sich der Typ des enthaltenen Objekts auch ändern, zum Beispiel wenn man die Variable auf ein anderes Objekt zeigen lässt:

Tier x = new Vogel();
...
x = new Katze(); // OK, da Katze Unterklasse von Tier
...
x = new Tier(); // natürlich auch OK

Daher nennt man den Typ des enthaltenen Objekts den dynamischen Typ, da dieser sich ändern kann.

In obigen Beispiel ändert sich der Typ des enthaltenen Objekts zwei Mal. Die Variable x bleibt dennoch immer vom Typ Tier. Deshalb nennt man den Typ, mit dem die Variable deklariert wurde (im Beispiel: Tier) also den statischen Typ. Der Typ des Objekts, das sich gerade in der Variable befindet nennt man den dynamischen Typ.

Tier x = new Vogel(); // stat. Typ = Tier, dyn. Typ = Vogel
...
x = new Katze(); // stat. Typ = Tier, dyn. Typ = Katze
...
x = new Tier(); // stat. Typ = Tier, dyn. Typ = Tier

Stellen wir uns nochmal die Frage, welche Methode im folgenden Code ausgeführt wird:

public static void main(String[] args) {
  Tier x = new Katze();
  x.sprich();
}

Die Variable x hat den statischen Typ Tier und den dynamischen Typ Katze. Jetzt können wir die Regel formulieren, nach der Java eine Methode aus der Klassenhierarchie auswählt:

Es wird immer die Methode des dynamischen Typs ausgeführt, falls vorhanden.

Falls die Methode dort nicht existiert, wird die Methode derjenigen Oberklasse gewählt, die dem dynamischen Typ am nächsten ist.

Casting

Nehmen wir an, wir haben folgende Klassenhierarchie. Jede Klasse hat die angegebenen Instanzvariablen und entsprechende Getter-Methoden:

Sie haben eine Variable für Medium und speichern dort einen Song:

Medium m = new Song("OMG", "Marteria");

Der statische Typ von m ist in diesem Fall Medium, der dynamische Typ ist Song.

Wenn Sie die Getter-Methode getArtist() aufrufen wollen, können Sie das nicht ohne weiteres tun, denn für den Zugriff auf Variablen und Methoden ist für Java immer der statische Typ maßgeblich:

Medium m = new Song("OMG", "Marteria");
String x = m.getArtist(); // Fehler!

Der Compiler (der Ihren Code in Bytecode übersetzt) weigert sich in diesem Fall, den Code zu übersetzen, da aus Sicht des Compilers nicht sicher ist, ob die Variable m ein Medium-Objekt, ein Song-Objekt oder ein Movie-Objekt enthält.

Für den Compiler ist immer der statische Typ maßgeblich, das heißt, der statische Typ bestimmt, welche Variablen und Methoden zur Verfügung stehen.

Wenn Sie als Programmierer sicher sind, dass die Variable m einen Song enthält, können Sie den Compiler zwingen, die Variable als Song-Typ zu betrachten. Man sagt: Sie casten die Variable auf die Unterklasse Song (von engl. to cast) Das sieht dann so aus:

Medium m = new Song("OMG", "Marteria");
Song s = (Song)m; // m auf Typ Song casten
String x = s.getArtist(); // jetzt OK

Man nennt das (Song) auch den Casting-Operator.

Das geht auch kürzer:

Medium m = new Song("OMG", "Marteria");
String x = ((Song)m).getArtist(); // Casting und Variablenzugriff

Die Klammerorgie ist leider notwendig, sonst wird der Casting-Operator (Song) erst nach dem Punkt ausgewertet, also zu spät.

instanceof

Manchmal möchten Sie prüfen, welchen dynamischen Typ eine Variable gerade hat. Stellen Sie sich vor, Sie erzeugen Objekte aufgrund einer Datei mit Song/Movie-Informationen und speichern sie in einem Array. Dann wissen Sie nicht, an welcher Stelle ein Song und an welcher Stelle ein Movie steht. Mit instanceof können Sie prüfen, ob ein Objekt von einem bestimmten Typ ist.

Medium[] media = readMedia("file.txt"); // hier werden Songs und Movies eingelesen
for (int i = 0; i < media.length; i++) {
  if (media[i] instanceof Song) {
    System.out.println("Artist: " + ((Song)media[i]).getArtist());
  }
}

Der Operator instanceof testet also, ob ein Objekt (linke Seite, im Beispiel media[i]) von einem Typ (rechte Seite, im Beispiel "Song") ist. Wäre media[i] von einem Subtyp von Song, würde der Operator auch true zurückgeben.

Processing-Beispiel, Teil 2

Sie erinnern sich hoffentlich an unser Beispiel mit den animierten Bällen und Quadraten aus Kap. 18.4:

Unser Ziel ist es, viele Objekte mit einer Liste zu bewegen:

ArrayList<Mover> things = new ArrayList<Mover>();

for (Mover m: things) {
  m.update();
  m.render(); // Fehler: Mover hat kein render
}

Problem ist: Die Klasse Mover hat keine Methode render(). Daher kann Java das render in der Schleife nicht aufrufen.

Die Lösung ist, eine "Dummy-Methode" in Mover einzuführen:

class Mover {
  PVector location;
  PVector speed;

  // Code zur Übersichtlichkeit ausgelassen

  void render() {
  }
}

Unser Klassendiagramm sieht jetzt so aus:

Wir können also jetzt die Methode render() in unserer Schleife aufrufen, da Mover ein render() besitzt:

ArrayList<Mover> things = new ArrayList<Mover>();

for (Mover m: things) {
  m.update();
  m.render();
}

Da wir wissen, dass jedes Element auf jeden Fall entweder vom Typ Ball oder vom Typ Quad ist, wird immer eine der zwei speziellen render-Methoden aufgerufen. Die Methode von Mover wird nie verwendet!

Aber: Unsere Lösung ist noch nicht optimal! Eine "Dummy-Methode" könnte einen nicht gut eingeweihten Programmierer dazu verführen, Code dort hineinzuschreiben, der aber nie ausgeführt wird. Zweites Problem ist die Klasse Mover selbst. Derselbe Programmierer könnte auf die Idee kommen, eine Instanz von Mover anzulegen. Das aber ist nicht sinnvoll, da Mover aus rein "organisatorischen" Gründen angelegt wurde, aber nicht dazu da ist, um Mover-Objekte zu erzeugen. Beide Probleme werden im nächsten Kapitel behoben.

Object: Mutter aller Klassen

Jede Klasse, die Sie selbst schreiben oder die Sie vorfinden, hat immer als "oberste" Oberklasse die Klasse Object. Hat Ihre Klasse keine Oberklasse, wird Object automatisch als Oberklasse definiert. Damit ist die Klasse Object die Wurzel jeder Klassenhierarchie und somit die "Mutter aller Klassen".

Warum braucht man das? Da alle Objekte in Java die Methoden der Klasse Object erben, kann man in Object Methoden vorgeben, die eben alle Objekte benötigen. Ein wichtiges Beispiel ist die Methode toString(), die Ihre Klasse automatisch erbt und die Sie überschreiben können, um die Ausgabe mit println() zu steuern. Weitere interessante Methoden sind equals() und hashCode(), das soll hier aber nicht vertieft werden.

Eine weitere Folge ist, dass ein Test mit instanceof Object bei jedem Objekt true ist.

Ein weiterer Vorteil ist, dass Variablen vom Typ Object jedes beliebige Objekt speichern können, da alle Typen automatisch Subtypen von Object sind.

// Beispiele mit fiktiven Klassen:
Object a = new Person();
Object b = new Flugzeug();
Object c = new Foo();

Zusammenfassung

  • Wenn Sie eine Variable vom Typ A anlegen, ist die Variable immer vom statischen Typ A.
  • Eine Variable vom Typ A kann auch Objekte von Typ B enthalten, sofern B Subtyp (Unterklasse) von A ist. Der konkrete Typ eines Objekts zur Laufzeit (also hier B) ist der dynamische Typ.
  • Man darf nur Methoden und Variablen des statischen Typs nutzen.
  • Mit instanceof können Sie den dynamischen Typ prüfen. Genauer gesagt ist x instanceof C genau dann wahr, wenn x vom Typ C oder von irgendeinem Subtyp von C ist.
  • Mit Casting können Sie den Compiler zwingen, eine Variable als einem Subtyp zugehörig aufzufassen, so dass Sie die Methoden und Variablen des entsprechenden (spezielleren) Subtyps verwenden können. Die Schreibweise ist (B)x, wenn Sie x auf B casten wollen

Da ein- und dieselbe Variable Objekte unterschiedlichen Typs speichern kann, spricht man von Polymorphie, zu deutsch "Vielgestaltigkeit".

Polymorphie (Vielgestaltigkeit) bedeutet in diesem Zusammenhang, dass der Aufruf x.render() unterschiedliche Ausprägungen annehmen kann, je nachdem, von welchem (dynamischen) Typ x ist.

Übungsaufgaben

(a) Klassenhierarchie und Casting

Schauen Sie sich folgenden Code an und zeichnen Sie die Klassenhierarchie als Diagramm.

public class Kuenstler {
}
public class BildenderKuenstler extends Kuenstler {
}
public class Maler extends BildenderKuenstler {

  public void male() {
    System.out.println("mal mal mal");
  }
}
public class Bildhauer extends BildenderKuenstler {

  public void haue() {
    System.out.println("kracks");
  }
}
public class Musiker extends Kuenstler {
  public void musiziere() {
    System.out.println("tralala");
  }
}

Verwenden Sie folgendes Hauptprogramm und rufen Sie auf jeder Variable die entsprechende Methode (male, haue, musiziere) auf, indem Sie Casting benutzen:

public static void main(String[] args) {
  Kuenstler k1 = new Maler();
  Kuenstler k2 = new Bildhauer();
  Kuenstler k3 = new Musiker();

  // Ihr Code...
}

(b) instanceof

Erstellen Sie folgende Klassen (Wenn Sie möchten, können Sie auch zur besseren Strukturierung eine Zusatzklasse erstellen).

  • Handy: Hat Eigenschaft preis (float)
  • IPhone: Unterklasse von Handy
  • AndroidPhone: Unterklasse von Handy, hat Eigenschaft hersteller (String)
  • WindowsPhone: Unterklasse von Handy, hat Eigenschaft hersteller (String)

Schreiben Sie für jede Klasse genau einen Konstruktor, verwenden Sie dabei super(), und schreiben Sie die toString()-Methode.

Im Hauptprogramm erstellen Sie eine ArrayList von Handys, fügen Sie von jeder Klasse zwei Beispielobjekte hinzu. Anschließend schreiben Sie mit einer Foreach-Schleife jedes Objekt mit println() auf die Konsole. Hier sollen Sie allerdings bei allen iPhones und WindowsPhones "AUSVERKAUFT" hinter die Ausgabe schreiben. Nutzen Sie dabei instanceof.

(c) Casten

Erstellen Sie folgende Klassen:

  • Animal: Hat Eigenschaft name (String)
  • Fox: Unterklasse von Animal, hat Methode hunt(), die lediglich "(name) is hunting" auf der Konsole ausgibt (name durch Variableninhalt ersetzen)
  • Rabbit: Unterklasse von Animal, hat Methode flee(), die lediglich "(name) is fleeing" auf der Konsole ausgibt (name durch Variableninhalt ersetzen)

Erstellen Sie eine Liste mit vier Beispielobjekten (2 Füchse, 2 Hasen). Durchlaufen Sie die Liste mit einer Foreach-Schleife und rufen Sie entweder hunt() oder flee() auf, je nachdem, welche Klasse Sie vor sich haben.

(e) Reisefieber

Sie schreiben ein Programm für eines Reiseportals zur Verwaltung der Reisen der Benutzer.

Achten Sie im folgenden auch auf die Verwendung von Paketen.

Schreiben Sie im Paket de.reisefieber.data die Klasse Reise mit den Eigenschaften ziel (String) und kostenTransport (double). Fügen Sie eine Gettermethode für kostenTransport hinzu (achten Sie auf die korrekte Namensgebung bei der Methode).

Schreiben Sie im gleichen Paket die Unterklasse ReiseMitHotel (Unterklasse von Reise) mit den Eigenschaften hotel (String) und kostenHotel (double). Fügen Sie auch hier eine Gettermethode für kostenHotel hinzu.

Schreiben Sie im Paket de.reisefieber.app die Klasse Kundenkonto. Diese enthält eine Liste von Reisen und die folgenden Methoden (sollten selbsterklärend sein):

  • neueReise(String ziel, double kosten)
  • neueReiseMitHotel(String ziel, double kosten, String hotel, double kostenHotel)
  • berechneKostenGesamt()
  • berechneKostenHotels()

Testen Sie Ihr Programm mit:

Kundenkonto konto = new Kundenkonto();

konto.neueReise("München", 50);
konto.neueReise("Berlin", 150);
konto.neueReise("London", 350);
konto.neueReiseMitHotel("Stuttgart", 80, "B&B", 60);
konto.neueReiseMitHotel("Berlin", 150, "Motel One", 70);

System.out.println("Hotelkosten: " + konto.berechneKostenHotels());
System.out.println("Gesamtkosten: " + konto.berechneKostenGesamt());

Sie sollten sehen:

Hotelkosten: 130.0
Gesamtkosten: 910.0

19.5 Abstrakte Klassen

Video: Abstrakte Klassen und Methoden (6:29)

Abstrakte Klassen und Methoden

In unserem MyTunes-Beispiel haben wir eine neue Klasse Medium eingeführt, damit wir sowohl Filme als auch Songs in einem Array repräsentieren können.

Es fällt auf, dass die Klasse Medium nie instanziieren wird. Das ist auch gut so, denn ein Objekt vom Typ Medium wäre zu unspezifisch, um sinnvoll nutzbar zu sein.

Abstrakte Klasse

Um zu verhindern, dass ein ahnungsloser Programmierer dennoch ein Medium-Objekt anlegt, können wir die Klasse als abstrakt deklarieren.

public abstract class Medium {
  ...
}

Mit dem Schlüsselwort abstract sagen wir Java, dass diese Klasse nie instanziiert werden darf. Eine abstrakte Klasse darf aber Instanzvariablen und Methoden beinhalten, die sie vererbt.

Abstrakte Methoden

In abstrakten Klassen können wir bestimmte Methoden von allen Unterklassen einfordern, ohne dass wir bereits Code hinschreiben. Eine solche Methode ohne Code, eigentlich nur das "Versprechen einer Methode", nennen wir abstrakte Methode. Auch diese wird mit dem Schlüsselwort abstract kenntlich gemacht.

Im Beispiel würde es Sinn machen, eine abstrakte Methode play() zu definieren, die dann in den Klassen Movie und Song erst implementiert wird (also dort erst Code bekommt).

public abstract class Medium {
  ...

  // alle Unterklassen müssen diese Methode implementieren:
  abstract void play();
}

Beachten Sie, dass Sie bei der abstrakten Methode keinen Zugriffsmodifikator angeben. Der Zugriff wird in den konkreten Unterklassen geregelt.

Die Unterklassen Movie und Song müssen play() implementieren, also Code haben (es sei denn, diese Klassen sind auch abstract):

public class Movie extends Medium {
  ...

  public void play() {
    // konkreter Code, um Film abzuspielen
  }
}

public class Song extends Medium {
  ...

  public void play() {
    // konkreter Code, um Song abzuspielen
  }
}

Dennoch könnte man im Hauptprogramm bei einer Variable vom Typ Medium play() aufrufen (Medium ist hier also der statische Typ). Dann würde eine der beiden play()-Methoden aufgerufen werden, je nachdem, ob der dynamische Typ Movie oder Song ist.

Nehmen wir an, Sie suchen in Ihrer Datenbank und bekommen ein Objekt vom Typ Medium zurück (statischer Typ). Jetzt dürfen Sie play() aufrufen, da es in Medium als abstrakte Methode definiert ist:

	Medium m = searchMedia("OMG");
	m.play(); // OK, da play() in Medium definiert
	

Wie wir oben gelernt haben, wird dabei das play() des aktuellen Typs genommen (dynamischer Typ), also von Song, wenn es ein Song ist, oder von Movie, wenn es ein Film ist.

Processing-Beispiel, Teil 3

Wir haben derzeit folgende Klassenstruktur:

Es gibt zwei Probleme:

  1. Man könnte Instanzen von Mover erzeugen, obwohl dies nicht erwünscht ist.
  2. Die Methode render() der Klasse Mover ist leer und existiert nur zu dem Zweck, dass die Methoden der Unterklassen "bekannt gemacht" werden.

Um Problem Nr. 1 zu lösen, können wir Mover zu einer abstrakten Klasse machen. Dazu müssen wir nur das Wort abstract vor die Klassendefinition schreiben:

abstract class Mover {
  ...
}

Wenn Sie jetzt versuchen, ein neues Objekt vom Typ Mover herzustellen, bekommen Sie eine Fehlermeldung.

void setup() {
  Mover m = new Mover(); // FEHLER
}

Genau das wollten wir erreichen! Eine abstrakte Klasse kann ansonsten Instanzvariablen und Methoden haben, die sie weitervererbt. Abstrakte Klassen sind also ideal, um gemeinsame Funktionalität "nach oben" zu verschieben.

Wie lösen wir Problem Nr. 2? Wir möchten einerseits Java signalisieren, dass alle Unterklasse die Methode render() haben, möchten aber andererseits keine "Dummy-Methode" einbauen.

Zu diesem Zweck gibt es abstrakte Methoden. Eine abstrakte Methode hat keinen Code, sondern ist lediglich ein Versprechen, dass alle (nicht-abstrakten) Unterklassen diese Methode implementieren. Das ganze schreibt man - innerhalb der abstrakten Klasse - dann so:

abstract void render();

Abstrakte Methoden können natürlich auch Parameter und Rückgabetyp definieren. Sie dürfen logischerweise nur in abstrakten Klassen vorkommen.

Hier nochmal der komplette Code von Mover:

abstract class Mover {
  PVector location;
  PVector speed;

  Mover() {
    location =
      new PVector(random(0, width), random(0, height));
    speed =
      new PVector(random(-3, 3), random(-3, 3));
  }

  void update() {
    location.add(speed);
    if (location.x > width || location.x < 0) {
      speed.x = -speed.x;
    }

    if (location.y > height || location.y < 0) {
      speed.y = -speed.y;
    }
  }

  abstract void render();
}

Im Klassendiagramm schreibt man bei abstakten Klassen "<<abstract>>" über den Klassennamen. Den Klassennamen schreibt man kursiv. Bei abstrakten Methoden schreibt man den Methodennamen kursiv.

Unser Code ist jetzt gut strukturiert und robust.

Zusammenfassung

Eine abstrakte Klasse verwendet man, um zu verhindern, dass Instanzen der Klasse erzeugt werden. Dazu stellt man das Schlüsselwort abstract vor die Klassendefinition.

Abstrakte Klassen können Instanzvariablen und Methoden enthalten und diese an die Unterklassen vererben.

Soll eine Methode einer abstrakten Klasse keinen Code enthalten, sondern lediglich als Versprechen dienen, dass alle nicht-abstrakten Unterklassen diese Methode enthalten, dann kann man eine abstrakte Methode definieren. Auch die abstrakte Methode wird mit dem Schlüsselwort abstract markiert.

Eine abstrakte Methode enthält keinen Code. Die Kopfzeile wird mit einem Semicolon abgeschlossen. Jede (nicht-abstrakte) Unterklasse muss diese Methode erfüllen, sonst kommt eine Fehlermeldung.

Übungsaufgaben

(a) Klassenhierarchie

Wir schauen uns die Klassen aus Aufgabe 19.4 (a) noch einmal an:

public class Kuenstler {
}
public class BildenderKuenstler extends Kuenstler {
}
public class Maler extends BildenderKuenstler {
  public void male() {
    System.out.println("mal mal mal");
  }
}
public class Bildhauer extends BildenderKuenstler {
  public void haue() {
    System.out.println("kracks");
  }
}
public class Musiker extends Kuenstler {
  public void musiziere() {
    System.out.println("tralala");
  }
}

Welche der Klassen können/sollten abstrakt sein? Zeichnen Sie dies korrekt in Ihrem Klassendiagramm ein.

(b) Animierte Objekte

Wir sehen uns den neuesten Stand unserer animierten Objekte an:

ArrayList<Mover> things = new ArrayList<Mover>();

void setup() {
  size(200, 200);
  things.add(new Ball());
  things.add(new Ball());
  things.add(new Quad());
  things.add(new Ball());
  things.add(new Quad());
}

void draw() {
  background(0);
  fill(255);
  noStroke();

  for (Mover m: things) {
    m.update();
    m.render();
  }
}

abstract class Mover {
  PVector location;
  PVector speed;

  Mover() {
    location =
      new PVector(random(0, width), random(0, height));
    speed = new PVector(random(-3, 3), random(-3, 3));
  }

  abstract void render();

  void update() {
    location.add(speed);
    if (location.x > width || location.x < 0) {
      speed.x = -speed.x;
    }

    if (location.y > height || location.y < 0) {
      speed.y = -speed.y;
    }
  }
}

class Ball extends Mover {
  void render() {
    ellipse(location.x, location.y, 20, 20);
  }
}

class Quad extends Mover {
  void render() {
    rectMode(CENTER);
    rect(location.x, location.y, 20, 20);
  }
}

Nun sollen Sie den Quad so ändern, dass er rotiert und sich ansonsten nicht im Raum bewegt.

Ändern Sie dazu die Klassenhierachie wie folgt:

  • Führen Sie eine neue Oberklasse Thing ein, die ab sofort die location enthält.
  • Führen Sie eine weitere Klasse Rotator ein, die zwei Eigenschaften enthält: angle und angularSpeed. Die Instanzvariable angularSpeed wird im Konstruktor zufällig gewählt (zwischen -0.1 und +0.1) und bestimmt, wie der Drehwinkel angepasst wird.
  • Zeichnen Sie eine sinnvolle Klassenhierarchie auf und bestimmen Sie, wo abstrakte Klassen/Methoden sinnvoll sind.
  • Implementieren Sie die Klassen und ändern Sie insbesondere Quad so ab, dass der Kasten rotiert (Denken Sie an Transformationen, vielleicht hilft auch push/popMatrix).