Behandelte Konzepte/Konstrukte: Oberklasse/Unterklasse, Subtypen, Klassendiagramm, Objektdiagramm, extends, super(), UTF-8, toString()

In diesem Kapitel müssen wir mal auf Grafik verzichten und einen weiteren Blick auf Klassen und Objekte werfen. Dazu entwerfen wir eine kleine Software, die Musiktitel und Filme verwalten kann.

13.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.

Unsere Klasse Song sieht wie folgt aus:

class Song {
  String title;
  String artist;
  String file;

  // Konstruktor
  Song(String aTitle, String aArtist, String aFile) {
    title = aTitle;
    artist = aArtist;
    file = aFile;
  }
}

Für Filme schreiben wir eine Klasse Movie:

class Movie {
  String title;
  String director;
  String file;

  // Konstruktor
  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.

void setup() {

  // 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) {
    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) {
    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/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.

class Movie {
  ...

  // Rückgabetyp muss String sein!
  String toString() {
    return "MOVIE \"" + title + "\" von " + director +
    ", zu finden unter: " + file;
  }
}

class Song {
  ...

  // Rückgabetyp muss String sein!
  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 und Song 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.

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 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.

13.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.

class A {
  int anum = 10;

  void afun() {
    println("a " + anum);
  }
}

Um Processing zu sagen, dass Klasse B die Unterklasse von A ist, benutzen wir das Schlüsselwort "extends" (engl. erweitert):

class B extends A {
  int bnum = -99;

  void bfun() {
    println("b " + anum + " " + bnum);
  }
}

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.

void setup() {
  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:

Die Klasse Medium hat einen eigenen Konstruktor.

class Medium {
  String title;
  String file;

  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:

class Movie extends Medium {
  String director;

  Movie(String aTitle, String aDirector, String aFile) {
  super(aTitle, aFile);
  director = aDirector;
  }

  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:

class Song extends Medium {
  String artist;

  Song(String aTitle, String aArtist, String aFile) {
    super(aTitle, aFile);
    artist = aArtist;
  }

  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:

void setup() {

  // 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) {
    println(m);
  }
}

Einschub: Der ternäre Operator statt If-Else

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:

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:

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;
println("Sie sind " + (alter < 40 ? "jung" : "alt"));

Übungsaufgaben

13.2 a) Hochschul-Personen

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. Anschließend schreiben Sie die Klasse Professor (auch Unterklasse von Person) mit der Instanzvariablen hasDrTitle (boolean).

Schreiben Sie Konstruktoren für die Klassen, bei denen das sinnvoll ist. Schreiben Sie für jede Klasse, wo das sinnvoll erscheint, eine aussagekräftige toString-Methode.

Um Ihre Klassen zu testen, erzeugen Sie vier Variablen a, b, c, d - jeweils vom Typ Person. In jeder Variable speichern Sie ein neues Objekt (je Student oder Professor). Anschließend geben Sie a, b, c und d jeweils mit 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
Tipp
Der Professoren- und Doktortitel sollte nicht in der Variablen name gespeichert werden, sondern im toString() erzeugt werden. Wenn Sie sich wundern, dass 007007 nicht so aussieht, wie es sein sollte, lesen Sie Kap. 2.3.7 in Java ist auch eine Insel .
Tipp
Schauen Sie sich den ternären Operator im obigen Kapitel 17.2 an.

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 println() auszugeben. Warum können Sie sowohl Student- als auch Professor-Objekte in dieser Liste speichern? Argumentieren Sie mit dem Subtyp.

13.2 b) Restaurant-Klassen

Sie sollen für ein italienisches Restaurant folgende Klassen programmieren (in Klammern stehen die Instanzvariablen, finden Sie jeweils einen sinnvollen Typ). Bringen Sie die Klassen in eine sinnvolle Ober-/Unterklassenbeziehung:

  • Pizza (preis, titel, belag, vegetarisch)
  • Gericht (preis, titel)
  • Pasta (preis, titel, sosse)

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.

13.2 c) Animierte Objekte

Schreiben Sie eine Klasse Ball für einen Ball, der an zufälliger Position beginnt und dann in zufälliger Richtung fliegt und von den Wänden abprallt.

Jetzt wollen wir eine zweite Klasse schreiben, die ein Quadrat gleichermaßen durch den Raum bewegt. Statt unsere Klasse Ball einfach zu kopieren, erschaffen Sie eine neue Oberklasse Mover. Welchen Code können Sie dorthin auslagern?

Am Ende haben Sie drei Klassen (Mover, Ball und Quad) und können den Code ausprobieren. Lassen Sie Bälle und Quadrate fliegen.

Tipp: Verwenden Sie eine Methode update zum bewegen/abprallen und eine Methode render zum Zeichnen.

Die Lösung ist weiter unten in Abschnitt 17.4 beschrieben...

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.

13.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.

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:

class Foo {

  // eine Testmethode
  void hello() {
    println("servus");
  }

}

Beim Erzeugen einer neuen Instanz wird der Konstruktor ohne Parameter aufgerufen - er wird von Processing hinzugefügt:

void setup() {
  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)...

class Foo {

  Foo(int x) {
  }

  void hello() {
    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.

class Bar {

  Bar() {
    println("Konstruktor von Bar");
  }

}

class Foo extends Bar {

  Foo() {
    // Processing führt hier super() aus
    println("Konstruktor von Foo");
  }

}

Wenn Sie eine Instanz von Foo erzeugen...

void setup() {
  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).

class Bar {

  // Konstruktor hat jetzt Parameter
  Bar(int x) {
    println("Konstruktor von Bar");
  }

}

class Foo extends Bar {

  Foo() {
    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.

class Bar {

  Bar(int x) {
    println("Konstruktor von Bar");
  }

}

class Foo extends Bar {

  Foo() {
    super(5);
    println("Konstruktor von Foo");
  }

}

13.4 Beispiel Animation

Schauen wir uns ein weiteres Beispiel aus dem Bereich Animation an.

Animatierte Objekte

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

13.4 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.