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 KlasseFoo
befinden und eine Instanzvariable namensx
haben, können Sie mitthis.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
19.2 a) (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
19.2 b) (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 sprich() 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:
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.
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, wennx
vom TypC
oder von irgendeinem Subtyp vonC
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 Siex
aufB
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
19.4 a) (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... }
19.4 b) (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
.
19.4 c) (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.
19.4 d) (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 instanziiert 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:
- Man könnte Instanzen von
Mover
erzeugen, obwohl dies nicht erwünscht ist. - 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
19.5 a) (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.
19.5 b) (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).