Lernziele
- Klassen in eine Ober-/Unterklassen-Beziehung setzen, um Vererbung zu nutzen
- Konstruktor der Oberklasse mit super() einbinden
- Den ternären Operator gezielt einsetzen, um den Code zu verschlanken
In diesem Kapitel lernen wir das Konzept von Ober- und Unterklasse kennen, um so Klassenhierarchie zu erstellen. Dazu entwerfen wir eine kleine Software, die Musiktitel und Filme verwalten kann.
18.1 MyTunes: Eine Musik- und Filmverwaltung
Unsere Software soll unsere Bibliothek an Musik- und Filmdateien verwalten. Das heißt konkret: wir können Titel suchen, abspielen und vielleicht auch Playlists anlegen.
Erster Ansatz
Da wir objektorientiert arbeiten, entwerfen wir je eine Klasse für (1) Songs und (2) Filme. Ein konkreter Song wird dann durch eine Instanz (Objekt) repräsentiert. Zu jedem Song wollen wir Titel, Künstler und Dateinamen auf der Festplatte speichern. Bei Filmen speichern wir Titel, Regisseur und Dateinamen.
Legen Sie zunächst ein neues Package an z.B.
de.hsa.mytunes
Jetzt schreiben wir unsere Klasse Song:
package de.hsa.mytunes; public class Song { // Instanzvariablen private String title; private String artist; private String file; // Konstruktor public Song(String aTitle, String aArtist, String aFile) { title = aTitle; artist = aArtist; file = aFile; } }
Für Filme schreiben wir eine Klasse Movie:
package de.hsa.mytunes; public class Movie { private String title; private String director; private String file; public Movie(String aTitle, String aDirector, String aFile) { title = aTitle; director = aDirector; file = aFile; } }
Um unsere Klassen zu testen, müssen wir
Testobjekte erzeugen, die wir in Listen speichern. Wir machen das in einer statischen main-Methode
in MyTunes
. Denken Sie daran, die ArrayList zu importieren!
package de.hsa.mytunes; import java.util.ArrayList; public class MyTunes {} public static void main(String[] args) { // Liste mit Test-Filmen ArrayList<Movie> movies = new ArrayList<Movie>(); movies.add(new Movie("Alien", "Ridley Scott", "alien.mov")); movies.add(new Movie("Der Pate", "Francis Ford Coppola", "pate.mov")); movies.add(new Movie("Gravity", "Alfonso Cuarón", "gravity.avi")); // Ausgeben for (Movie m: movies) { System.out.println(m); } // Liste mit Test-Songs ArrayList<Song> songs = new ArrayList<Song>(); songs.add(new Song("OMG!", "Marteria", "omg.wav")); songs.add(new Song("Happy", "Pharrell Williams", "happy.wav")); // Ausgeben for (Song s: songs) { System.out.println(s); } } }Als Ausgabe sehen Sie:
MyTunes$Movie@3fa3e565 MyTunes$Movie@7aa16cf5 MyTunes$Movie@13e36e92 MyTunes$Song@6b398cec MyTunes$Song@1a8e93ba
So sieht es aus, wenn Java (und Processing) Objekte ausgibt.
Objektausgabe mit toString()
Die Ausgabe ist leider nicht sehr aussagekräftig. Deshalb
bedienen wir uns einer Technik, um die Ausgabe von Objekten
schöner zu machen: wenn Sie in einer Klasse die Methode toString()
hinzufügen, dann wird diese Methode aufgerufen, sobald das
Objekt mit print()
ausgegeben wird.
public class Movie { ... // Rückgabetyp muss String sein! public String toString() { return "MOVIE \"" + title + "\" von " + director + ", zu finden unter: " + file; } } public class Song { ... // Rückgabetyp muss String sein! public String toString() { return "SONG \"" + title + "\" von " + artist + ", zu finden unter: " + file; } }
Falls Sie sich über das
"MOVIE \""
wundern, das wird gleich erklärt.
Die Ausgabe ist jetzt schöner:
MOVIE "Alien" von Ridley Scott, zu finden unter: alien.mov MOVIE "Der Pate" von Francis Ford Coppola, zu finden unter: pate.mov MOVIE "Gravity" von Alfonso Cuarón, zu finden unter: gravity.avi SONG "OMG!" von Marteria, zu finden unter: omg.wav SONG "Happy" von Pharrell Williams, zu finden unter: happy.mp3
Escape-Sequenzen in Strings
Wenn Sie in einem String Anführungszeichen einbauen wollen, dann haben Sie ein Problem: Wie verhindern Sie, dass Processing denkt, dass an dieser Stelle der String zu Ende ist:
String foo = "dies ist ein Anführungszeichen: ""; // Fehler
Sie müssen das vorletzte Anführungszeichen kennzeichnen, so dass Processing weiß, dass es im String ist und nicht die Begrenzung darstellt. Dieses Kennzeichnen funktioniert mit einem vorangestelltem Backslash:
String foo = "dies ist ein Anführungszeichen: \""; // OK
Analog funktioniert das für "unsichtbare" Zeichen wie Tab (\t), Zeilenumbruch (\n) und den Backslash selbst (\\).
Probleme und Code-Duplizierung
Unser Programm hat einige unschöne Aspekte:
-
Die Klassen
Movie
undSong
sehen fast gleich aus. Das bedeutet, dass Code doppelt vorhanden ist. Man nennt das Code-Duplizierung. Das gilt als schlechte Programmierpraxis, weil ich bei Änderungen in einem Code (z.B. einen Fehler beheben oder den Code schneller/eleganter gestalten) auch den anderen ändern müsste. - Wir müssen die Medien in zwei Listen verwalten, besser wäre alles in einer Datenstruktur.
Ein wichtiges Designprinzip vom Erstellen von Klassen ist also die Vermeidung von Code-Duplizierung, im Englischen spricht man auch von DRY: Don't Repeat Yourself. Im nächsten Kapitel lernen wir eine Möglichkeit kennen, Code-Duplizierung zu vermeiden.
Übungsaufgaben
18.1 a) (a) toString
Schreiben Sie eine Klasse Ausflug
für eine Ausflugs-Planungsapp. Die Klasse soll das Ziel, die Anzahl der Personen und das Datum speichern (für das Datum einfach einen String verwenden). Achten Sie auf die korrekten Zugriffsmodifikatoren.
Fügen Sie eine toString
Methode hinzu, so dass Sie eine "schöne" Ausgabe bekommen, z.B.
Ausflug nach "Königsbrunn" am 24.06.2020 mit 6 Personen Ausflug nach "Ulm" am 1.04.2020 mit 12 Personen Ausflug nach "Innsbruck" am 12.10.2022 mit 20 Personen
Zusammenfassung
Wir haben Klassen für Lieder (Klasse Song) und Filme (Klasse Movie) geschrieben. Wenn wir in einer Klasse eine Methode namens toString()
implementieren (mit einem String als Rückgabe), können wir die Objekte mit System.out.println() in verständlicher Form ausgeben.
Wir haben ein Problem identifiziert: Beide Klassen beinhalten ähnlichen Code, d.h. wir haben es hier mit Code-Duplizierung zu tun. Dies sollte vermieden werden.
18.2 Klassenhierarchie und Vererbung
In objektorientierten Sprachen kann man zwei Klassen A und B zueinander in Beziehung setzen, so dass A die Oberklasse von B ist. Die Konsequenz lehnt sich an die Eltern-Kind-Beziehung beim Menschen an: Die Unterklasse "erbt" alle Instanzvariablen und Methoden der Oberklasse. Das bedeutet technisch, dass die Unterklasse zunächst mal alle Instanzvariablen und Methoden der Oberklasse benutzen kann, als wären sie in der eigenen Klasse definiert worden.
Man kann sich das anhand verschiedener Typologien verdeutlichen. Zum Beispiel: ein Auto (Klasse) ist ein Fahrzeug (Klasse). Auto erbt von Fahrzeug die Eigenschaft, dass es sich fortbewegen kann. Ein Auto hat aber auch Eigenschaften, die die Oberklasse (Fahrzeug) nicht hat, z.B. dass es vier Räder hat. Ein Fahrrad (Klasse) könnte eine weitere Unterklasse von Fahrzeug sein und hat die Eigenschaft, zwei Räder zu besitzen. Das ganze kann man in einem Klassendiagramm festhalten.
Video: Klassendiagramm/Objektdiagramm (8:43)
Die Pfeile bedeuten "ist Unterklasse von" und müssen genau so gezeichnet werden (d.h. offene Spitze, durchgezogener Strich), denn diese Diagramme sind standardisiert.
Ist-ein
Eine Klassenhierarchie zu erstellen ist ein Designproblem, d.h. es gibt nicht immer ein richtig oder falsch. Eine gute Daumenregel, um herauszufinden, ob die Klassenbeziehungen stimmen ist die "ist-ein"-Regel. Man sollte immer sagen können "<Unterklasse> ist ein <Oberklasse>". Im obigen Beispiel also "Auto ist ein Fahrzeug" und "Fahrrad ist ein Fahrzeug".
Einfaches Beispiel
Definieren wir mal eine einfache Klasse A mit einer Eigenschaft und einer Methode.
public class A { protected int anum = 10; public void afun() { System.out.println("a " + anum); } }
Wir verwenden hier einen neuen Zugriffsmodifikator namens "protected". Warum wir das tun, wird gleich klar.
Um Processing zu sagen, dass Klasse B die Unterklasse von A ist, benutzen
wir das Schlüsselwort extends
(engl. erweitert):
public class B extends A { private int bnum = -99; public void bfun() { System.out.println("b " + anum + " " + bnum); // Zugriff auf anum } }
Sie sehen hier, dass die Klasse B auf die Instanzvariable anum
zugreifen kann, obwohl diese Variable doch eigentlich zu A gehört!
Der Grund ist, dass B diese Variable geerbt hat,
d.h. in B gibt es genauso eine Variable anum. Damit dieser Zugriff wirklich funktioniert,
mussten wir allerdings den Zugriffsmodifikator von "private" auf das schwächere "protected" umstellen,
sonst dürfte selbst die Unterklasse B nicht auf die Variable zugreifen.
public static void main(String[] args) { A a = new A(); B b = new B(); a.afun(); b.bfun(); b.afun(); }
Beachten Sie, dass man auf
b
auch die Methoden der Oberklasse aufrufen kann,
in diesem Fall
afun
. Auch die Methoden vererbt A an B.
a 10 b 10 -99 a 10
Typen und Subtypen
Sobald Sie eine Klasse A schreiben, erzeugen Sie auch einen neuen Datentypen A, denn Sie können natürlich dann Variablen herstellen, die Instanzen von A speichern sollen.
Wenn Klasse B Unterklasse von A ist, dann ist der Datentyp B automatisch Subtyp von A. Das bedeutet, dass eine Variable von Typ A auch Objekte von Typ B speichern kann.
Beispiel 1:
A a = new A(); B b = new B(); A a2 = b; // korrekt!
Beispiel 2: Nehmen wir an, es gäbe die Klassen Fahrzeug, Auto und Fahrrad, wie oben skizziert.
Fahrzeug f; Auto a = new Auto(); f = a; // korrekt Fahrzeug f2 = new Fahrrad(); // ebenso!
Das ist korrekt, denn: ein Auto ist ein Fahrzeug und ein Fahrrad ist ein Fahrzeug. Umgekehrt geht es nicht:
Auto a; a = new Fahrzeug(); // falsch!
denn ein Fahrzeug ist nicht (immer) ein Auto.
Video: Klassenhierarchie und Vererbung (12:55)
MyTunes 2: Vererbung nutzen
Mit unserem neuen Wissen verbessern wir unsere Datenbank. Da die Instanzvariablen vererbt werden, können wir die gemeinsamen Variablen in eine neue Oberklasse "Medium" auslagern:
Was wird vererbt? Die Klasse Movie
erbt die Eigenschaften title und file, hat also insgesamt die drei Eigenschaften:
- title
- file
- director
Die Klasse Song
erbt ebenfalls die Eigenschaften title und file, hat also insgesamt die drei Eigenschaften:
- title
- file
- artist
Daher kann man auch sagen, dass die Unterklasse Song
ihre Oberklasse erweitert (engl. to extend).
Der Klasse Medium
geben wir einen eigenen Konstruktor:
public class Medium { private String title; private String file; public Medium(String aTitle, String aFile) { title = aTitle; file = aFile; } }
Verwendung von super()
Um Code-Duplizierung zu vermeiden, können Sie im Konstruktor einer Klasse den Konstruktor der Oberklasse aufrufen. In unserem Beispiel werden die Variablen title und file im Konstruktor von Medium initialisiert. Wir wollen diesen Code nicht im Konstruktor von Movie duplizieren.
Also rufen wir im Konstruktor von Movie den Konstruktor der Oberklasse mit
super()
auf. super() hat immer die gleiche Anzahl von Parametern
wie der Konstruktor (bzw. einer der Konstruktoren) der Oberklasse.
In diesem Fall sind das zwei Parameter:
public class Movie extends Medium { private String director; public Movie(String aTitle, String aDirector, String aFile) { super(aTitle, aFile); director = aDirector; } public String toString() { return "MOVIE \"" + title + "\" von " + director + ", zu finden unter: " + file; } }
Wichtig: super() muss immer die erste Anweisung im Konstruktor sein. Es gibt übrigens noch eine weitere Verwendung von super, um Methoden der Oberklasse aufzurufen. Letzteres wird aber ohne Klammern geschrieben und behandeln wir später. Nicht verwechseln!
Analog für die Klasse Song:
public class Song extends Medium { private String artist; public Song(String aTitle, String aArtist, String aFile) { super(aTitle, aFile); artist = aArtist; } public String toString() { return "SONG \"" + title + "\" von " + artist + ", zu finden unter: " + file; } }
Wir passen den Teil an, wo wir die Listen erzeugen:
Jetzt können wir alle Objekte in einer Liste vom Typ
Medium
ablegen,
da
Movie
und
Song
Subtypen von Medium sind.
Die Verwaltung wird so wesentlich übersichtlicher:
public static void main(String[] args) { // Nur noch eine Liste für beide Medientypen ArrayList<Medium> media = new ArrayList<Medium>(); media.add(new Movie("Alien", "Ridley Scott", "alien.mov")); media.add(new Movie("Der Pate", "Francis Ford Coppola", "pate.mov")); media.add(new Movie("Gravity", "Alfonso Cuarón", "gravity.avi")); media.add(new Song("OMG!", "Marteria", "omg.wav")); media.add(new Song("Happy", "Pharrell Williams", "happy.wav")); // Nur noch eine For-Schleife zur Ausgabe for (Medium m: media) { System.out.println(m); } }
Einschub: Der ternäre Operator statt If-Else
Wir benötigen den sogenannten ternären Operator für einige Übungsaufgaben. Außerdem ist es gut, ihn zu kennen, weil es ihn in vielen Programmiersprachen gibt.
Es gibt Fälle, wo das If-Else etwas sperrig ist. Nehmen wir an, Ihr Programm soll ausgeben, ob jemand jung (< 40 Jahre) oder alt (>= 40 Jahre) ist:
public String altersklasse(int alter) { if (alter < 40) { return "jung"; } else { return "alt"; } }
Der sogenannte ternäre Operator erlaubt es Ihnen, diese drei Bestandteile (Bedingung, Fall 1, Fall 2) sehr kompakt in folgender Form zu schreiben:
BEDINGUNG ? FALL1 : FALL2
In unserem Beispiel wäre dies:
public String altersklasse(int alter) { return alter < 40 ? "jung" : "alt"; }
Der ternäre Operator wird auch häufig verwendet, um Strings zusammenzusetzen. Dann müssen Sie den ganzen Ausdruck einklammern:
int alter = 50; System.out.println("Sie sind " + (alter < 40 ? "jung" : "alt"));
Übungsaufgaben
18.2 a) (a) Ternärer Operator
Schreiben Sie die Klasse Diplom
, um die guten alten Diplome auszudrucken.
Dabei unterscheiden wir zwischen Fachhochschul-Diploma (FH) und Universitäts-Diploma (Univ).
Die Klasse hat folgende Eigenschaften (unbedingt die Typen beachten!):
- person: ein String mit dem Namen des/der Absolventen/in
- mitAuszeichnung: boolean
- fachhochschule: boolean
Schreiben Sie einen Konstruktor mit einem Parameter (für die Person) und zwei Setter-Methoden für die booleschen Eigenschaften.
Schließlich schreiben Sie eine toString
-Methode, so dass eine Ausgabe in folgender Art herauskommt:
FH-Diplom für Lisa Schmidt Univ-Diplom für Donald Duck mit Auszeichnung FH-Diplom für Daisy Duck mit Auszeichnung
Sie sollten mit folgendem Testcode (in der statischen main-Methode) den obigen Output erzeugen können:
Diplom d1 = new Diplom("Lisa Schmidt"); d1.setFachhochschule(true); Diplom d2 = new Diplom("Donald Duck"); d2.setMitAuszeichnung(true); Diplom d3 = new Diplom("Daisy Duck"); d3.setFachhochschule(true); d3.setMitAuszeichnung(true); System.out.println(d1); System.out.println(d2); System.out.println(d3);
18.2 b) (b) Eine Unterklasse
Schreiben Sie die Klasse Person
mit den Instanzvariablen name und isMale (boolean). Schreiben Sie die Klasse Student
als Unterklasse von Person mit der Instanzvariablen matrikelnummer.
Schreiben Sie Konstruktoren für beide Klassen.
Schreiben Sie für die Klasse Student
eine aussagekräftige toString-Methode.
Um Ihre Klassen zu testen, erzeugen Sie zwei Variablen a und b vom Typ Person (nicht vom Typ Student). In jeder Variable speichern Sie ein neues Student-Objekt. Anschließend geben Sie a und b mit System.out.println() aus. Die Ausgabe könnte so aussehen:
Lisa Müller, Studentin, Matrikelnr. 809221 Augustin Burg, Student, Matrikelnr. 007007
18.2 c) (c) Mehrere Unterklassen
Nehmen Sie den Code aus (b) und erweitern Sie ihn.
Schreiben Sie die neue Klasse Professor
(ebenfalls Unterklasse von Person) mit der Instanzvariablen hasDrTitle (boolean), mit Konstruktor und mit toString-Methode.
Um alle Klassen zu testen, erzeugen Sie vier Variablen a, b, c, d - jeweils vom Typ Person (nicht vom Typ Student). In jeder Variable speichern Sie ein neues Objekt (je Student oder Professor). Anschließend geben Sie a, b, c und d jeweils mit System.out.println() aus. Die Ausgaben für vier Objekte könnten so aussehen:
Lisa Müller, Studentin, Matrikelnr. 809221 Professor Dr. Rainer Unsinn Augustin Burg, Student, Matrikelnr. 007007 Professorin Hilde Brün
Jetzt legen Sie im Hauptprogramm eine ArrayList
an (von welchem Typ?) und fügen Sie ihr Objekte hinzu. Verwenden Sie anschließend eine Foreach-Schleife, um die Objekte mit System.out.println() auszugeben. Warum können Sie sowohl Student- als auch Professor-Objekte in dieser Liste speichern? Argumentieren Sie mit dem Subtyp.
18.2 d) (d) Restaurant-Klassen
Sie sollen für ein italienisches Restaurant folgende Klassen programmieren (in Klammern stehen die Instanzvariablen, finden Sie jeweils einen sinnvollen Typ):
- Pizza (preis, titel, belag, vegetarisch)
- Gericht (preis, titel)
- Pasta (preis, titel, sosse)
Bringen Sie die Klassen in eine sinnvolle Ober-/Unterklassenbeziehung und verteilen Sie entsprechend die Eigenschaften auf die Klassen.
Programmieren Sie die drei Klassen. Fügen Sie eine aussagekräftige toString-Methode hinzu und testen Sie Ihre Klassen im Hauptprogramm, indem Sie einige Gerichte anlegen und auf der Konsole ausgeben.
Zusammenfassung
Zwei Klassen A und B können in einer Beziehung zueinander stehen. Wenn A Oberklasse von B ist, dann erbt Klasse B alle Instanzvariablen und Methoden von A. Man sagt auch, dass B Unterklasse von A ist. Im Englischen spricht man von superclass und subclass.
Im Code kennzeichnet man dies nur in der Unterklasse mit dem Stichwort extends
:
class A { ... } class B extends A { ... }
Wenn B Unterklasse von A ist, dann ist B auch Subtyp von A. Das bedeutet ein Objekt vom Typ B ist gleichzeitig vom Typ A. Wenn die Klasse Animal
Oberklasse von Hase
ist, dann ist ein Objekt vom Typ Hase
gleichzeitig vom Typ Animal
. Folgender Code ist also korrekt:
Animal x = new Hase(); // korrekt
Im Konstruktor der Unterklasse kann man mit super()
einen beliebigen Konstruktor der Oberklasse aufrufen. Die super-Anweisung muss allerdings die erste Anweisung im Konstruktor sein.
Wir konnten unseren MyTunes-Code erheblich verbessern, indem wir die Oberklasse Medium
einführten. Der gemeinsame Code von Song und Movie liegt jetzt in der Oberklasse, die Code-Duplizierung wurde eliminiert.
18.3 Vererbung allgemein
Im obigen Beispiel haben wir eine einfache Klassenhierarchie gesehen. Schauen wir uns eine etwas komplexere an:
In diesem Klassendiagramm sehen Sie für jede Klasse die Instanzvariablen und die Methoden in eigenen Bereichen. Etwas ungewohnt ist die Angabe der Datentypen nach dem Format
<Variablenname> : <Typ>
Das hängt damit zusammen, dass Klassendiagramme auch für andere Programmiersprachen als Java/Processing verwendet werden und jede Programmiersprache eine andere Schreibweise hat.
Zugriffsmodifikatoren 2
Sie wissen bereits, dass public
bedeutet: Jeder kann auf die Klasse/Variable zugreifen,
insbesondere können andere Objekte darauf zugreifen. Dagegeben heißt private
, dass
nur ein Objekt dieser Klasse darauf zugreifen kann. Wichtig ist, dass bei private selbst eine Unterklasse nicht auf die Variable/Methode zugreifen kann.
Jetzt gibt es noch einen weiteren Modifikator: protected
. Mit
diesem Modifikator dürfen auch die Unterklassen auf die Variable/Methode zugreifen.
Zusätzlich gilt (und das ist vielleicht etwas kontra-intuitiv): auch alle Klassen innerhalb des gleichen Pakets haben Zugriff.
Nochmal die folgende Tabelle, die zusammefasst, wer Zugriff auf eine(n) Klasse, Konstruktor, Methode oder Instanzvariable hat:
Modifikator | Eigene Klasse | Unterklassen | Klassen im gleichen Paket | Alle Klassen |
---|---|---|---|---|
private | X | |||
protected | X | X | X | |
public | X | X | X | X |
(leer) | X | X |
Sie sehen hier auch, was passiert, wenn Sie den Modifikator ganz weglassen ("leer"). Diesen Fall nennt man package-private. Hier haben die Unterklassen keinen Zugriff, dafür aber alle Klassen im selben Paket. Diese Kombination ist selten sinnvoll, daher sollten Sie immer einen Modifikator hinschreiben.
Oberklassen und direkte Oberklassen
Sie wissen, dass eine Unterklasse C alle Instanzvariablen und Methoden von Ihrer Oberklasse B erbt. Wenn Klasse B wiederum eine Oberklasse A hat, dann erbt C diese Dinge auch von A! Im Beispiel oben erbt Klasse Auto auch die Instanzvariablen und Methoden der Klasse Produkt.
Dies liegt daran, dass Produkt auch eine Oberklasse von Auto ist. Allgemein kann man sagen:
WENN: A ist Oberklasse von B UND B ist Oberklasse von C DANN GILT: A ist Oberklasse von C
(Diese Eigenschaft nennt man Transitivität.)
Dann gilt allgemein: Eine Klasse erbt die Instanzvariablen und Methoden von all ihren Oberklassen. Im Beispiel oben nennt man übrigens die Klasse Fahrzeug die direkte Oberklasse von Auto, um anzuzeigen, dass keine andere Klasse zwischen den beiden ist.
Jede Klasse hat nur eine direkte Oberklasse
Eine wichtige Regel in Java ist, dass jede Klasse maximal eine direkte Oberklasse haben darf. Das heißt, das hier ist nicht erlaubt (obwohl es im echten Leben durchaus Sinn macht):
Warum nicht? Man könnte doch einfach die Eigenschaften und Methoden von beiden Oberklassen erben. Probleme gibt es aber, wenn die zwei Oberklassen Variablen haben, die genau gleich heißen, oder Methoden haben, die die gleiche Signatur haben. Welche Variable soll Java wählen? In Java hat man dies also verboten, aber in anderen Sprachen wie C++ oder LISP ist es durchaus erlaubt, erfordert aber Zusatzmechanismen (siehe Wikipedia Mehrfachvererbung).
Konstruktoren: Wichtige Regeln
Sie wissen, dass man pro Klasse mehrere Konstruktoren definieren kann. Sie wissen, dass man mit super()
einen Konstruktor der Oberklasse aufrufen kann.
Hier noch der Vollständigkeit halber ein paar Regeln.
Regel 1: Wenn in einer Klasse kein Konstruktor definiert ist, wird automatisch ein leerer Konstruktor ohne Parameter definiert.
Sie können also folgendes definieren:
public class Foo { // kein Konstruktor! // eine Testmethode public void hello() { System.out.println("servus"); } }
Beim Erzeugen einer neuen Instanz wird der Konstruktor ohne Parameter aufgerufen - er wird von Processing hinzugefügt:
public static void main(String[] args) { Foo foo = new Foo(); foo.hello(); }
servus
Regel 2: Sobald mindestens ein eigener Konstruktor definiert wird, wird der Konstruktor ohne Parameter nicht automatisch generiert.
Erweitern Sie die obige Klasse um einen Konstruktor (der Parameter x hat keine Funktion)...
public class Foo { public Foo(int x) { } public void hello() { System.out.println("servus"); } }
... dann können Sie das Programm nicht mehr ausführen. Sie erhalten die Fehlermeldung:
The constructor ...Foo() is undefined
Regel 3: Hat Ihre Klasse eine Oberklasse, dann wird in allen Konstruktoren automatisch super()
aufgerufen, sobald eine Instanz erzeugt wird - sofern nicht ein eigenes super()
vom Programmierer eingefügt wurde.
public class Bar { public Bar() { System.out.println("Konstruktor von Bar"); } } public class Foo extends Bar { public Foo() { // Java führt hier super() aus System.out.println("Konstruktor von Foo"); } }
Wenn Sie eine Instanz von Foo
erzeugen...
public static void main(String[] args) { Foo foo = new Foo(); }
...sehen Sie, dass zuerst der Konstruktor der Oberklasse aufgerufen wird. Erst dann wird der Code des Konstruktors von Foo
ausgeführt:
Konstruktor von Bar Konstruktor von Foo
Ein subtiler Fehler tritt auf, wenn Sie in der Oberklasse einen eigenen Konstruktor mit Parameter/n definieren. Es wird kein Konstruktor ohne Parameter automatisch definiert (Regel 2).
public class Bar { // Konstruktor hat jetzt Parameter public Bar(int x) { System.out.println("Konstruktor von Bar"); } } public class Foo extends Bar { public Foo() { System.out.println("Konstruktor von Foo"); } }
Wenn Sie wieder eine Instanz von Foo
anlegen wollen, versucht Processing den Konstruktor ohne Parameter von Bar
aufzurufen, der ja nicht existiert. Dies endet mit einem Fehler:
Implicit super constructor ...Bar() is undefined.
Um den Fehler zu beheben, gibt es drei Möglichkeiten: (a) Sie fügen einen leeren Konstruktor zu Bar
hinzu, (b) Sie löschen den eigenen Konstruktor, so dass der parameterlose Konstruktor automatisch erzeugt wird oder (c) Sie rufen selbst super()
auf, allerdings mit Parameter.
public class Bar { public Bar(int x) { System.out.println("Konstruktor von Bar"); } } public class Foo extends Bar { public Foo() { super(5); System.out.println("Konstruktor von Foo"); } }
18.4 Processing-Beispiel
Die gleichen Techniken funktionieren auch in Processing! Schauen wir uns ein Beispiel aus dem Bereich Animation an.
Wir haben den typischen, sich bewegenden und abprallenden Ball als Klasse:
class Ball { PVector location; PVector speed; // Konstruktor mit zufälligen Anfangswerten // für Startpunkt und Geschwindigkeit Ball() { location = new PVector(random(0, width), random(0, height)); speed = new PVector(random(-3, 3), random(-3, 3)); } // Zeichnen void render() { ellipse(location.x, location.y, 20, 20); } // Koordinaten anpassen und das Abprallen regeln 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; } } }
Das Hauptprogramm könnte so aussehen (wichtig ist, dass der Konstruktor in setup aufgerufen wird und nicht im Variablenteil darüber, weil dort height und width noch nicht gesetzt sind).
Ball b; void setup() { size(200, 200); b = new Ball(); } void draw() { background(0); fill(255); noStroke(); b.update(); b.render(); }
Jetzt stellen Sie sich vor, Sie möchten ein weiteres Objekt fliegen lassen, ein Quadrat. Im Grunde wäre der Code fast identisch.
Einziger Unterschied: die Methode render() funktioniert bei Quad anders, da die natürlich ein Quadrat zeichnet...
class Quad { PVector location; PVector speed; Quad() { location = new PVector(random(0, width), random(0, height)); speed = new PVector(random(-3, 3), random(-3, 3)); } void render() { rectMode(CENTER); rect(location.x, location.y, 20, 20); } 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; } } }
Wieder hilft uns die Klassenhierachie. Wir führen eine neue Klasse ein, die alle gemeinsamen Elemente enthält:
- Standort (location)
- Geschwindigkeitsvektor (speed)
- update-Methode
Wir nennen die neue Oberklasse Mover
:
Im Code verschieben wir alle Funktionalität in Mover
, bis auf die Methode render
natürlich:
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; } } }
Die Klassen Ball
und Quad
sind Unterklassen von Mover
und enthalten wesentlich weniger Code:
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); } }
Mit folgendem Hauptprogramm können Sie den Code testen:
void setup() { size(200, 200); b = new Ball(); q = new Quad(); } void draw() { background(0); fill(255); noStroke(); b.update(); q.update(); b.render(); q.render(); }
Die obige Klasse Mover
könnte in einem Computerspiel eine Menge verschiedener Unterklassen haben: Hinternisse, Bösewichter, Lebensrationen und Waffen... Doch wir haben ein Problem: wie verwalten wir all diese Objekte effizient? Für jeden Bestandteil des Spiels eine eigene Variable anzulegen, wäre kaum machbar. Wir hätten viel lieber eine Liste:
ArrayList<Mover> things = new ArrayList<Mover>();
Dann könnten wir in draw() einfach alle Objekte mit einer For-Schleife updaten und zeichnen:
for (Mover m: things) { m.update(); m.render(); // Fehler: Mover hat kein render }
Zurzeit geht das nicht, weil die Klasse Mover
keine Methode render
hat. Muss auch so sein, denn die Klasse Mover hat ja keine bestimmte Form, die sie zeichnen kann. Wir können dieses Problem erst im nächsten Kapitel mit Hilfe von abstrakten Klassen und abstrakten Methoden lösen.
Übungsaufgabe
18.4 a) (a) Spielerfigur
Übernehmen Sie den Code aus dem obigen Abschnitt mit den Klassen Mover, Ball und Quad.
Sie möchten eine Spielerfigur einführen (Klasse Player). Diese wird als Dreieck gezeichnet und soll mit den Cursortasten zu bedienen sein. Das heißt, die Klasse Player kann zwar die Variable location gebrauchen, nicht aber speed (und auch nicht update).
Wie müssen Sie die Klassen umbauen, dass Player nur die Variable location erbt?
Tipp: Führen Sie eine neue Klasse ein.