Letztes Update: 29.06.2017
Behandelte Befehle: Klasse, null, Methode, Konstruktor, Punktnotation, Zeiger, Referenz

11.1 Klassen und Objekte

Nachdem Sie schon Objekte des Typs PVector und String verwendet haben, lernen Sie jetzt, wie Sie eigene Klassen schreiben.

Das interessanteste an Objekten ist ihre Wiederverwendbarkeit. Diese Wiederverwendbarkeit hat dazu geführt, dass eine Sprache wie Java (Processing) erweitert wird, indem eine Reihe von Klassen von vornerein bereit gestellt wird. Sie kennen z.B. die Klasse String, die bei Java von Haus aus zur Verfügung steht. Eine Kollektion von Klassen nennt man auch Bibliothek. Die in Java "eingebaute" Bibliothek nennt man die Java-Standardbibliothek.

Video: Objekte 1 (12:07)

Eine Klasse ist ein zunächst Mal abstraktes Konzept wie z.B. der Mensch oder das Haus. Für jede Klasse gibt es konkrete Instanzen, z.B. der konkrete Mensch Barack Obama oder ein konkretes Haus wie Ihre derzeitige Wohnstätte. Eine Instanz nennt man in unserem Kontext auch Objekt.

Eine Klasse definiert bestimmte Eigenschaften, z.B. hat ein Mensch ein Alter und eine Haarfarbe, ein Haus hat eine Adresse. Aber die Eigenschaften einer Klasse haben zunächst keine Werte. Erst die entsprechenden Objekte/Instanzen haben konkrete Werte für diese Eigenschaften. In Processing sind die Eigenschaften als Variablen abgebildet.

Klasse definieren / Instanzvariablen

Um eine Klasse zu definieren, erzeugen Sie in Processing einen neuen Reiter (Tab) mit dem Namen der Klasse, z.B. Ball. Einen neuen Reiter erzeugen Sie mit dem kleinen Dreieck neben dem Hauptreiter:

Anschließend geben Sie den Namen für den neuen Reiter ein, z.B. Ball. Sie sehen den neuen Reiter oben:

Beachten Sie, dass Klassennamen (und die entsprechenden Reiter) immer groß geschrieben werden sollten!

Um eine Klasse Ball mit drei Eigenschaften zu definieren, schreiben Sie:

class Ball
{
   int xpos;
   int ypos;
   int d; // Durchmesser
}

Die Eigenschaften nennt man auch Instanzvariablen, da es Variablen sind, die immer jeweils einem Objekt (einer Instanz) gehören.

Eine Klasse kann man sich als abstrakten Bauplan im plantonischen Raum der Ideen vorstellen:

Objekte erzeugen (Instanziieren)

Sobald Sie an anderer Stelle im Code eine Instanz einer Klasse benötigen, müssen Sie zunächst Variablen deklarieren, die eine solche Instanz speichern können:

Ball ball;
Ball foo;
Ball einBall;

Die Klasse Ball ist gleichzeitig ein Datentyp (genauso wie int oder String). Man sagt daher: "Variable foo ist vom Typ Ball".

Jetzt können Sie neue Instanzen erzeugen und jeweils in den Variablen ablegen:

ball = new Ball();
foo = new Ball();
einBall = new Ball();

In dem Beispiel werden also drei Ball-Objekte erzeugt. Sie können das natürlich auch jeweils auf einer Zeile ausführen:

Ball ball = new Ball();
Ball foo = new Ball();
Ball einBall = new Ball();

Der Prozess des Instanziierens führt dazu, dass ein neues Objekt (eine Instanz) erschaffen wird. Wir stellen uns das als Häuschen in Object City vor. Jedes Objekt hat konkrete Werte für die Eigenschaften xpos, ypos und d.

Die Variablen ball, foo und einBall enthalten nicht etwa diese Häuschen, sondern nur die Adresse des Häuschens. Man sagt auch, dass die Variable auf das Objekt zeigt.

Deshalb können Sie ein Objekt auch nicht einfach auf der Konsole ausgeben:

Ball ball = new Ball();
println(ball);
sketch_151202a$Ball@404f1faf

Was Sie sehen, ist so etwas wie die Adresse des Häuschens, aber Sie interessieren ja die Eigenschaften. Wenn Sie die sehen wollen, müssen Sie per Punktnotation auf sie zugreifen.

Variablenzugriff per Punktnotation

Um auf die Eigenschaften zuzugreifen, bedienen wir uns der Punktnotation. Hier setzen wir erstmal die Eigenschaften der drei Objekte von oben auf bestimmte Werte:

ball.xpos = 100;
ball.ypos = 100;
ball.d = 40;
foo.xpos = 50;
foo.ypos = 30;
foo.d = 20;
einBall.xpos = 20;
einBall.ypos = 10;
einBall.d = 20;

Processing/Java benötigt die Punktnotation, um dasjenige Objekt (Häuschen) zu finden, bei dem die Änderung vorgenommen werden soll:

Sofern man die Werte eines Objekts das erste Mal setzt, nennt man das auch ein Objekt initialisieren. Die Eigenschaften eines Objekts können sich natürlich während des Programmablaufs ändern, z.B. die Variablen xpos und ypos in dem Beispiel, sofern die Objekte animiert werden sollen. Mit dem Begriff Zustand meint man die aktuelle Belegung aller Eigenschaften (Instanzvariablen) eines Objekts. Der Zustand kann sich also fortwährend ändern.

Der Wert null

Wird eine Variable angelegt, ohne dass ein Objekt darin gespeichert wird, befindet sich der Platzhalter null darin. Sie können das explizit angeben:

Ball ball2 = null;
println(ball2);

Sie sehen:

null

Das Symbol null bedeutet, dass die Variable ball2 gerade gar nicht auf ein Objekt zeigt:

Das wiederum heißt, dass Sie nicht per Punktnotation auf Instanzvariablen zugreifen können:

Ball ball2 = null;
ball2.xpos = 15; // Fehler! ball2 zeigt nirgendwo hin

Dies führt zu der Fehlermeldung NullPointerException. Sie müssen also bei Variablen, die Objekte beinhalten, immer aufpassen, dass dort nicht null enthalten ist.

Übungsaufgaben

(a) Klasse Student

Schreiben Sie eine Klasse Student mit den Eigenschaften (Instanzvariablen): name (ein String) und matrikelnummer (Zahl).

Erzeugen Sie zwei Student-Objekte, setzen Sie die Eigenschaften (z.B. mit "Harry" und 123 für den einen und "Sally" und 456 für den anderen). Geben Sie anschließend die Eigenschaften mit println() auf der Konsole aus.

(b) Klasse Position

Schreiben Sie eine Klasse Position mit den Eigenschaften (Instanzvariablen) x und y vom Typ int.

Erzeugen Sie zwei Positions-Objekte und zeichnen Sie jeweils Kreise an den Positionen. Lassen Sie die Kreise diagonal über den Bildschirm fliegen, indem Sie mit Punktnotation auf x und y zugreifen.

Hinweis: Denken Sie daran, dass Sie, sobald Sie Klassen verwenden, im aktiven Modus sein müssen (setup/draw). Beachten Sie außerdem, dass Klassen immer groß geschrieben werden. Erzeugen Sie auch einen entsprechenden Reiter (Tab) mit exakt dem gleichen Namen wie die Klasse.

Hinweis: Sobald Sie animieren, müssen Sie überlegen, wo Sie Ihre Positions-Objekte erstellen (müssen global sein!) und wo Sie die Objekte befüllen (setup/draw).

(c) Adressbuch

Schreiben Sie eine Klasse, die ein Adressbuch speichern kann.

Die Klasse heißt Adressbuch und hat zwei Instanzvariablen: namen und tel. Beide sind String-Arrays.

Die Variable namen soll zu Beginn die Strings "Larry", "Lisa" und "Harry" enthalten. Die Variable tel soll "111 222", "333 444" und "012 345" enthalten.

Testen Sie Ihre Klasse mit:

void setup() {
  Adressbuch a = new Adressbuch();

  for (int i = 0; i 

Sie sollten sehen:

Larry: 111 222
Lisa: 333 444
Harry: 012 345

Zusammenfassung

Eine Klasse ist ein abstrakter Bauplan für konkrete Objekte. Objekte nennt man auch Instanzen. Die Klasse Mensch kann zum Beispiel zwei konkrete Instanzen haben: Harry und Sally.

Eine Klasse definiert Eigenschaften wie name (vom Typ String). Diese Eigenschaften funktionieren wie Variablen, gehören aber zu dieser bestimmten Klasse. Ein Objekt hat dann diese Eigenschaft/Variable und besitzt einen konkreten Wert für diese Eigenschaft, für die Eigenschaft name z.B. "Harry" oder "Sally".

Eine Klasse wird wie folgt definiert:

class Mensch {
    String name;
    int alter;
}

Sie erzeugen neue Instanzen der Klasse Mensch mit new Mensch(). Diesen Vorgang nennt man auch Instanziieren.

Gespeichert wird eine Instanz in einer Variablen vom Typ Mensch. Eine Variable, die eine Instanz speichert, zeigt nur auf diese Instanz. Man sagt, die Variable hat eine Referenz auf das Objekt.

Auf Eigenschaften, die auch Instanzvariblen genannt werden, wird über Punktnotation zugegriffen:

void setup() {
    Mensch a = new Mensch();
    Mensch b = new Mensch();
    a.name = "Harry";
    a.alter = 18;
    println(a.name + " " + a.alter);

    b.name = "Sally";
    ...
}

11.2 Methoden

Methoden definieren

Objekte können auch Aktionen beinhalten. Ein Mensch kann sich z.B. fortbewegen oder etwas sagen. Zu beachten ist, dass sich nur ein konkreter Mensch (Objekt) fortbewegen kann, nicht die ganze Klasse! In Processing kann man Funktionen in Klassen einbetten. Solche Funktionen werden oft Methoden genannt.

Schreiben Sie den Methodennamen immer klein!

class Ball
{
   int xpos;
   int ypos;
   int durchmesser;

   void zeichne() {
      ellipse(xpos, ypos, durchmesser, durchmesser);
   }

   void bewege() {
      xpos++;
   }
}

Methoden werden genauso wie Funktionen definiert, d.h. sie haben einen Namen, mehrere (oder keine) Parameter und einen Rückgabetyp.Sie müssen innerhalb der geschweiften Klammern der Klassendefinition definiert werden. Innhalb der Methoden kann man auf alle Instanzvariablen der Klasse zugreifen (im Beispiel: xpos, ypos, durchmesser).

Hier ein Beispiel einer Methode mit Parametern:

class Ball
{
   ...

   void setPosition(int xp, int yp) {
     xpos = xp;
     ypos = yp;
   }

}

Hier ein Beispiel einer Methode mit Rückgabe:

class Ball
{
   ...

   float distToMouse() {
      return dist(mouseX, mouseY, xpos, ypos);
   }

}

Methoden verwenden

Um eine Methode aufzurufen, bedient man sich ebenfalls der Punktnotation:

Ball ball = new Ball();

ball.xpos = 100;
ball.ypos = 100;
ball.durchmesser = 40;

ball.zeichne(); // Methode zeichne() dieses Objekts aufrufen

Übungsaufgaben

(a) Sprechmaschine

Schreiben Sie die Klasse Robot mit der Instanzvariable name (String).

Die Klasse hat drei Methoden:

  • stellDichVor(): schreibt "Hallo, ich heiße NAME" auf die Konsole (mit dem Namen eingesetzt)
  • sagWetter(): sagt (zufällig) eines von "Das Wetter wird gut.", "Bald gibt es Regen..." oder "Ich kann schon Wolken sehen."
  • tschues(): schreibt "NAME sagt auf Wiedersehen!"

Erzeugen Sie zwei Objekte, setzen Sie die Namen (per Punktnotation) und rufen Sie jeweils die drei Methoden auf.

(b) Klasse Tropfen

Schreiben Sie eine Klasse Tropfen mit den Eigenschaften x und y. Hinzu kommen die zwei Methoden zeichne() und bewege(). In der Methode zeichne() wird ein Tropfen an der Position x, y gezeichnet. In bewege() wird die Position verändert, um eine Animation zu erreichen.

Erstellen Sie anschließend zwei Tropfen (in setup) und lassen Sie sie von oben nach unten fallen (Aufrufe in draw).

Wenn die Tropfen unten ankommen, sollen Sie oben wieder eintreten.

Hinweise: Sie müssen hier überlegen, wo Sie Ihre Tropfenvariablen erstellen (müssen global sein!) und wo Sie Ihre Methodenaufrufe positionieren (setup/draw).

(c) Farbige Tropfen

Erweitern Sie die Klasse Tropfen um drei int-Werte für die Farben Rot, Grün, Blau (RGB) und färben Sie den Tropfen entsprechend ein.

Schreiben Sie in der Klasse Tropfen eine Methode neueFarbe(). Mit ihr sollen die drei RGB-Werte zufällig gesetzt werden.

Rufen Sie die Methode bei Tastendruck für beide Tropfen auf. Beide Tropfen sollten sich zufällig (und unterschiedlich) einfärben.

(d) Student / vorstellen

Erweitern Sie die Klasse Student um die Methode vorstellen(). Diese Methode soll Namen und Matrikelnummer wie folgt auf die Konsole schreiben (hier wird vorstellen() bei zwei Instanzen aufgerufen):

Hallo, ich bin Harry (123).
Hallo, ich bin Sally (456).

(e) Auto

Schreiben Sie die Klasse Auto mit den folgenden Eigenschaften (überlegen Sie sich passende Namen):

  • Kilometerstand (float)
  • Tankfüllung (float)
  • Durchschnittlicher Verbrauch (float) als Liter pro 100 km

Schreiben Sie anschließend die folgenden Methoden:

  • Reichweite (float): gibt zurück, wieviele Kilometer noch gefahren werden können
  • Nächste Inspektion (float): gibt zurück, wieviel km noch bis zur nächsten Inspektion gefahren werden können (Annahme: alle 20000 km)

Achten Sie darauf, dass die Methoden Werte zurückgeben.

Testen Sie Ihre Klasse und Methoden mit verschiedenen Testobjekten und geeigneten Werten.

Schreiben Sie anschließend eine Methode status(), die die Informationen zu Reichweite und nächster Inspektion auf der Konsole ausgibt.

(f) Adressbuch Teil 2

Verwenden Sie die Klasse Adressbuch aus dem vorigen Übungsblock mit den dort definierten drei Namen und Telefonnummern.

Erweitern Sie die Klasse um die Methode findeTel. Diese Methode bekommt einen Namen (String) und gibt die dazugehörige Telefonnummer (String) zurück.

Wenn der Name nicht im Telefonbuch steht, soll der String "unbekannt" zurückgegeben werden.

Testen Sie Ihren Code (in setup) mit:

String tel1 = a.findeTel("Lisa");
String tel2 = a.findeTel("Horst");
println("suche Lisa: " + tel1);
println("suche Horst: " + tel2);

Sie sollten sehen:

suche Lisa: 333 444
suche Horst: unbekannt

11.3 Konstruktoren

Video: Objekte 2 - Konstruktor (8:33)

Sobald man new Ball() aufruft, wird eine spezielle Methode der Klasse aufgerufen: der Konstruktor. Ein Konstruktor ist wie eine Methode und wird genau dann aufgerufen, wenn das Objekt erzeugt wird. Er wird häufig verwendet, um die Eigenschaften zu setzen (Initialisierung).

Der sogenannte leere Konstruktor wird automatisch erzeugt, er ist immer da, tut aber nichts.

Einfacher Konstruktor

Sie können selbst einen Konstruktor schreiben, um z.B. Ihre Eigenschaften auf einen Anfangszustand zu setzen:

class Ball
{
  int xpos;
  int ypos;
  int durchmesser;

   // Ihr eigener Konstruktor
   Ball()
   {
      xpos = 50;
      ypos = 50;
      durchmesser = 20;
   }

   // hier kommen die weitere Methoden
}

Ein Konstruktor sieht aus wie eine Methode, heißt aber immer genauso wie die Klasse, wird also (anders als Methoden) groß geschrieben.

Konstruktor mit Parametern

Sie können dem Konstruktor auch Parameter mitgeben, wenn Sie z.B. den Durchmesser übergeben wollen:

class Ball
{
  int xpos;
  int ypos;
  int durchmesser;

   // Ihr eigener Konstruktor
   Ball(int x, int y, int d)
   {
      xpos = x;
      ypos = y;
      durchmesser = d;
   }
}

Dieser Konstruktor wird bei der Instanziierung wie folgt aufgerufen:

Ball ball = new Ball(70, 60, 30);

Sie können mehrere Konstruktoren gleichzeitig definieren. Jeder dieser Konstruktoren ist dann bei der Instanziierung verwendbar:

class Ball
{
  int xpos;
  int ypos;
  int durchmesser;

   // Konstruktor 1 für Faule
   Ball()
   {
      xpos = 50;
      ypos = 50;
      durchmesser = 20;
   }

   // Konstruktor 2 für Raffinierte
   Ball(int x, int y, int d)
   {
      xpos = x;
      ypos = y;
      durchmesser = d;
   }
}

Dann könnten Sie folgendermaßen Instanziieren:

Ball ball = new Ball(70, 60, 30); // Konstruktor 2
Ball foo = new Ball(); // Konstruktor 1
Ball einBall = new Ball(); // Konstruktor 1

Übungsaufgaben

Die folgenden Aufgaben beziehen sich auf Klassen aus den vorigen Aufgaben.

(a) Konstruktor für den Tropfen

Schreiben Sie einen Konstruktor für die Klasse Tropfen, bei dem x und y übergeben werden.

(b) Leerer Konstruktor für den Tropfen

Schreiben Sie einen zusätzlichen Konstruktor für die Klasse Tropfen, wo x zufällig gesetzt wird (y auf 0).

Der neue Konstruktor kann mit dem obigen Konstruktor ko-existieren.

Hinweis: Beachten Sie, dass width erst dann gesetzt ist, wenn setup() ausgeführt wird (vorher ist es 0). Das heißt, beim Aufruf des Konstruktors im Bereich der globalen Variablen...

Tropfen foo = new Tropfen();

...ist width noch unbekannt (bzw. gleich Null). Sie müssen also Deklaration der Variablen dort lassen, aber das Erzeugen des Objekts in setup() durchführen.

(c) Student / Konstruktor

Schreiben Sie zwei Konstruktoren für die Klasse Student. Beim ersten Konstruktor werden Name und Matrikelnummer gesetzt. Beim zweiten wird nur der Name übergeben, die Matrikelnummer wird immer auf -1 gesetzt.

11.4 Interaktion zwischen Objekten

Abhängigkeiten

Objekte können Abhängigkeiten untereinander aufweisen. Zum Beispiel möchte ein Autohaus eine Kunden-Datenbank anlegen von verkauften Autos und ihren jeweiligen Besitzern. Dazu benötigt man Klassen für Autos und Personen. Zum Beispiel hier die Klasse Kunde:

class Kunde {
  String name;
  String adresse;

  Kunde(String n, String a) {
    name = n;
    adresse = a;
  }
}

Die Klasse Auto soll neben Marke und Farbe auch den Besitzer enthalten. Jetzt könnten wir den Namen als String verwenden, aber das wäre umständlich, wenn ich vom Auto ausgehend alle Daten des Benutzers finden möchte. Stattdessen speichere ich einfach das dazugehörige Kundenobjekt.

class Auto {
  String marke;
  String farbe;
  Kunde besitzer;

  Auto(String m, String f, Kunde k) {
    marke = m;
    farbe = f;
    besitzer = k;
  }
}

Der Vorteil, hier das Kundenobjekt zu speichern und nicht nur den Namen als String ist folgender: ich kann jetzt auf alle Informationen des Objekts zugreifen - hier z.B. die Adresse - ohne dass ich das entsprechende Objekt vorher suchen muss.

Ich kann jetzt eine Beschreibung hinzufügen, in der ich auch die Adresse des Kunden verwende:

class Auto {

  ...

  void beschreibe() {
    println(besitzer.name + " aus " + besitzer.adresse +
    " hat einen " + marke + " in " + farbe);
  }
}

Eine Beispielanwendung:

void setup() {
  // Kunden erstellen
  Kunde barack = new Kunde("Barack Obama", "Amerika");
  Kunde angie = new Kunde("Angela Merkel", "Deutschland");

  // Autos erstellen
  Auto bmw = new Auto("BMW", "rot", angie);
  Auto porsche = new Auto("Porsche", "schwarz", barack);
  Auto rover = new Auto("Land Rover", "schwarz", barack);

  // Autos ausgeben
  bmw.beschreibe();
  porsche.beschreibe();
  rover.beschreibe();
}
Angela Merkel aus Deutschland hat einen BMW in rot
Barack Obama aus Amerika hat einen Porsche in schwarz
Barack Obama aus Amerika hat einen Land Rover in schwarz

Im Grunde haben Sie hier nichts neues gelernt. Sie wussten bereits, dass Variablen Objekte "enthalten" können (besser gesagt: sie zeigen auf sie). Jetzt haben Sie gesehen, dass auch Instanzvariablen auf Objekte zeigen können.

Im Diagramm könnte man die Situation wie folgt darstellen:

Es ist immer wichtig, sich klar zu machen, dass die Variablen lediglich Zeiger auf Objekte sind. Wenn Sie z.B. aus einem Auto-Objekt heraus ein Kundenobjekt manipulieren, dann gilt diese Änderung natürlich "überall", also ach, wenn Sie später den Kunden über die Variable angie zugreifen.

Punktnotation

Die Punktnotation kann mehrfach hintereinander eingesetzt werden und sollte dies auch, weil sich dadurch sehr lesbarer Code ergibt:

void setup() {

  Kunde barack = new Kunde("Barack Obama", "Amerika");
  Kunde angie = new Kunde("Angela Merkel", "Deutschland");

  Auto bmw = new Auto("BMW", "rot", angie);
  Auto porsche = new Auto("Porsche", "schwarz", barack);
  Auto rover = new Auto("Land Rover", "schwarz", barack);

  println("Kunde des BMW: " + bmw.besitzer.name);
}
Kunde des BMW: Angela Merkel

Dies nennt man auch eine Verkettung von Punktnotation.

Übungsaufgaben

(a) Punktnotation

Gegeben seien die zwei Klassen Auto und Kunde (s.o.) und der folgenden Code:

void setup() {
  Kunde barack = new Kunde("Barack Obama", "Amerika");
  Kunde angie = new Kunde("Angela Merkel", "Deutschland");

  Auto bmw = new Auto("BMW", "rot", angie);
  Auto porsche = new Auto("Porsche", "schwarz", barack);
  Auto rover = new Auto("Land Rover", "schwarz", barack);
}

Ergänzen Sie Code, so dass die Orte, an denen sich die Eigentümer der drei Autos befinden, auf die Konsole gedruckt werden.

(b) Funktion

Gegeben sei der gleiche Code wie in (a).

Schreiben Sie eine Funktion gleicherBesitzer, die zwei Autoobjekte bekommt und einen Wahrheitswert zurückgibt. Die Rückgabe soll genau dann wahr sein, wenn die zwei Autos demselben Besitzer gehören.

Testen Sie Ihre Funktion in setup() mit:

println(gleicherBesitzer(bmw,porsche));
println(gleicherBesitzer(rover,porsche));

Sie sollten sehen

false
true

11.5 Array von Objekten

Video: Array von Objekten (5:42)

Wenn wir viele Objekte erzeugen und verwenden wollen, ziehen wir Arrays hinzu. Wie funktioniert ein Array von Objekten?

Array-Variable deklarieren

Ganz einfach: Da eine Klasse wie "Ball" genauso ein Datentyp ist wie "int" und "float", kann man auch einen Array dieses Typs definieren:

Ball[] balls;

Array erzeugen

Doch Vorsicht, der Array existiert noch nicht. Wir müssen ihn erst erzeugen, z.B. für fünf Objekte:

balls = new Ball[5];

Array mit Objekten befüllen

Jetzt haben wir ein Array mit fünf leeren Eimern, die jeweils ein Objekt vom Typ "Ball" aufnehmen können. Zu Beginn enthalten die Eimer lediglich den Wert null:

balls = new Ball[5];
println(balls[2]);
null

Wir müssen also zunächst die leeren Eimer befüllen, entweder einzeln...

balls[0] = new Ball();
balls[1] = new Ball();
balls[2] = new Ball();
balls[3] = new Ball();
balls[4] = new Ball(); // letzter Eimer!

... oder (besser) mit einer Schleife:

for (int i = 0; i < balls.length; i++) {
   balls[i] = new Ball();
}

Schauen wir uns mal eine bestimmte Klasse an, die auch ein bisschen was kann:

// Klasse Ball (in eigenem Reiter "Ball")
class Ball
{
  float xpos;
  float ypos;
  float xspeed;
  float yspeed;

  // Konstruktor
  // erzeugt Ball an zufälliger Position
  // mit zufälliger Geschwindigkeit
  Ball() 
  {
    xpos = random(10, width-10);
    ypos = random(10, height-10);
    xspeed = random(0,3);
    yspeed = random(0,3);
  }

  // Methode zum Zeichnen
  void render() {
    noStroke();
    ellipse(xpos, ypos, 20, 20);
  }

  // Methode zum Bewegen, inkl. Kollision
  void move() {
    xpos += xspeed;
    ypos += yspeed;

    if (xpos < 10 || xpos > width-10) {
      xspeed = -xspeed;
    }
    if (ypos < 10 || ypos > height-10) {
      yspeed = -yspeed;
    }
  }
}

Objekt-Methoden aufrufen

Diese Klasse können wir wie oben gezeigt in ein Array tun. Damit die Bälle auch gezeichnet und bewegt werden, müssen wir alle Bälle regelmäßig in der draw()-Methode des Hauptprogramms "aufgerufen" werden. Das komplette Hauptprogramm sähe wie folgt aus:

// Hauptprogramm
Ball[] balls = new Ball[5]; // neues Array

void setup() {
  // Array befüllen mit Objekten
  for (int i = 0; i < balls.length; i++) {
    balls[i] = new Ball();
  }
}

void draw() {
  background(100);

  // Funktionen der Objekte regelmäßig aufrufen
  for (int i = 0; i < balls.length; i++) {
    balls[i].render();
    balls[i].move();
  }
}

Arrays und Objekte sind eine mächtige Kombination, weil Sie so Ihre Computerwelt sehr leicht mit vielen komplexen Objekten bevölkern können, egal ob es sich um Spielfiguren, Dokumente oder Personeneinträge eines Sozialen Netzes handelt.

Beispiel: Personen

Sie können Objekte z.B. verwenden, um eine Personendatenbank anzulegen und zu durchsuchen. Zunächst definieren wir eine Klasse Person:

class Person {
  String name;
  int alter;
  float groesse;

  Person(String n, int a, float g) {
    name = n;
    alter = a;
    groesse = g;
  }
}

Jetzt legen Sie einen Array von Beispielpersonen an:

void setup() {
  Person[] personal = new Person[4];
  personal[0] = new Person("Harry", 36, 1.80);
  personal[1] = new Person("Sally", 26, 1.71);
  personal[2] = new Person("Angie", 56, 1.60);
  personal[3] = new Person("Jerry", 15, 1.75);
}

Schauen Sie sich jetzt die passenden Übungen dazu an.

Übungsaufgaben

(a) Ausgabe und Filtern

Erzeugen Sie die Klasse Person und einen Array mit Beispielpersonen wie oben gezeigt. Also z.B.:

void setup() {
  Person[] personal = new Person[4];
  personal[0] = new Person("Harry", 36, 1.80);
  personal[1] = new Person("Sally", 26, 1.71);
  personal[2] = new Person("Angie", 56, 1.60);
  personal[3] = new Person("Jerry", 15, 1.75);

  // Ihr Code hier...
}

Geben Sie nur die Namen aller Personen mit Hilfe einer Schleife aus.

Geben Sie anschließend nur die Namen aller Personen, die älter als 30 sind, auf der Konsole aus.

(b) Filtern als Funktion

Schreiben Sie eine Funktion printByAge, die zwei Parameter bekommt: einen Personenarray und eine Zahl x. Die Funktion druckt die Namen aller Personen auf der Konsole aus, die älter als x Jahre sind. Die Funktion hat keine Rückgabe.

void setup() {
  Person[] personal = new Person[4];
  personal[0] = new Person("Harry", 36, 1.80);
  personal[1] = new Person("Sally", 26, 1.71);
  personal[2] = new Person("Angie", 56, 1.60);
  personal[3] = new Person("Jerry", 15, 1.75);

  // Hier Funktion testen
  printByAge(personal, 30);
}

// Hier Funktion schreiben

Hinweis: Ein Array einer Klasse kann genauso wie z.B. ein int-Array übergeben werden, indem Sie als Parametertyp Person[] angeben. Achten Sie darauf, dem Parameter möglichst einen neuen Namen zu geben (also nicht "personal" nennen, so heißt der Testarray).

Wichtig: Beachten Sie, dass dies eine normale Funktion ist, also auf der gleichen Ebene wie setup/draw definiert wird und nicht innerhalb der Klasse Person.

Verwenden Sie den Array von oben für Ihre Tests. Denken Sie daran, keine globalen Variablen zu verwenden!

(c) Filtern als Funktion mit Rückgabe

Schreiben Sie eine Funktion getByAge, die zwei Parameter bekommt: einen Personenarray und eine Zahl x. Im Gegensatz zur vorigen Funktion gibt diese Funktion einen Personenarray zurück.

Sie müssen also einen neuen Array erzeugen und die Personen, die das entsprechende Alter haben, dort hinzufügen. Wie bekommen Sie heraus, wie groß Ihr Ergebnisarray sein muss?

Wenn Sie Ihre Funktion testen, bekommen Sie einen Array zurück, den Sie nicht ohne weiteres mit println ausdrucken können. Stattdessen müssen Sie das Ergebnis erst auffangen und dann mit einer For-Scheife durchgehen, um z.B. die Namen auszudrucken:

void setup() {
  Person[] personal = new Person[4];
  personal[0] = new Person("Harry", 36, 1.80);
  personal[1] = new Person("Sally", 26, 1.71);
  personal[2] = new Person("Angie", 56, 1.60);
  personal[3] = new Person("Jerry", 15, 1.75);

  // Hier Funktion testen
  Person[] result = getByAge(personal, 30);
  // jetzt For-Schleife zum Ausgeben der Namen
}

// Hier Funktion schreiben

Verwenden Sie wieder den Array von oben für Ihre Tests. Testen Sie auch Extremfälle (keine Person / alle Personen).

11.6 Objekte und Zeiger

Video: Variablen und Objekte (5:09)

Variablen, die ein Objekt enthalten, enthalten in Wirklichkeit nur die Adresse des Objekts. Dies hat wichtige Konsequenzen, wie wir gleich sehen werden. Schauen wir uns zwei Variablen mit Objekten an:

Ball b1 = new Ball(10, 20);
Ball b2 = new Ball(9, 99);

Wobei die Klasse Ball wie folgt aussieht:

class Game {
   int xpos;
   int ypos;

   Game(int x, int y) {
      xpos = x;
      ypos = y;
   }
}

Eine Variable, die ein Objekt aufnehmen kann, enthält nicht wirklich das Objekt selbst, sondern nur seine Adresse im Speicher. Man sagt auch, die Variable speichert eine Referenz oder einen Zeiger (engl. pointer) auf das Objekt. Man nennt eine Klasse deshalb auch einen Referenztyp. Man kann sich das so vorstellen:

Wenn Sie eine Zuweisung zwischen Objektvariablen ausführen, wird lediglich die Adresse eingefüllt. Im folgenden Fall zeigen beide Variablen anschließend auf das identische Objekt:

b2 = b1; // Adresse vom b1 wird in b2 gespeichert

Dann haben wir folgende Situation:

Weil b1 und b2 auf das selbe Objekt zeigen, kann man sowohl mit b1 als auch mit b2 dieses eine Objekt manipulieren. Wenn Sie schreiben...

b1.xpos = 5;
b2.ypos = 55;
println(b1.xpos);
println(b1.ypos);

...wird bei beiden Zuweisungen das selbe Objekt manipuliert:

Ihre Ausgabe sieht entsprechend aus:

5
55

Denn es gibt nur ein einziges Objekt. Beide Variablen, b1 und b2, zeigen auf dies eine Objekt. Egal, mit welcher Variable Sie dies Objekt manipulieren, so wird immer nur dies eine Objekt manipuliert und der zuletzt gesetzte Wert für eine Instanzvariable gilt.

Sie können beliebig viele weitere Variablen auf dies Objekt zeigen lassen:

Ball b3 = b1;
Ball b4 = b2;
Ball b5 = b1;
Ball b6 = b2;

Dabei ist es egal, welche andere Variable Sie als "Zulieferer" für die Adresse verwenden. Es steht überall das gleiche drin.

Primitive Variablen

Die Beschäftigung mit Objekten kann dazu führen, dass Sie Ihre Intuition bei primitiven Datentypen (int, float, boolean...) verlieren und hier Fehler machen.

Schauen Sie sich konkret folgenden Code an.

int a = 1;
int b = 0;

Sie haben zwei Variablen mit unterschiedlichen Werten. Was passiert jetzt:

b = a;

Variable b wird auf den Wert von a gesetzt. Wenn Sie b nochmals ändern...

b = 99;

...stellt sich die Frage, ob sich a auch ändert. Das ist nicht der Fall - es gibt keine unsichtbare Verbindung zwischen a und b!

Es werden in jedem Schritt Werte gesetzt. Es wird nicht mit Referenzen/Zeigern gearbeitet, es gibt keine "Häuschen". Man kann auch sagen: Primitive Variablen haben keine Identität (= Häuschen), sondern enthalten lediglich "primitive" Werte.

Objekte als Parameter

Wichtig ist das bei Funktionsaufrufen. Sehen wir uns zunächst eine Funktion mit einem Parameter an, der einen primitiven Datentyp (int) angehört:

void setup() {
   int x = 5;
   foo(x);
   println(x);
}

void foo(int p) {
   p = 10;
}
5

Ihre Variable x wird durch den Funktionsaufruf nicht geändert, denn Processing kopiert den Wert 5 in die Parametervariable p. Dies hat dann nichts mehr mit x zu tun.

Die Situation ändert sich, wenn Sie das gleiche mit einem Objekt der Klasse Ball tun:

void setup() {
   Ball b = new Ball(1, 2);
   boo(b);
   println(b.xpos);
}

void boo(Ball ball) {
   ball.xpos = 9;
}

Der Parameter ball ist nur eine weitere (lokale) Variable, die nach Funktionsaufruf auf die Adresse des einzigen bislang erzeugten Objekts zeigt:

Das heißt, alles, was Sie mit dem Objekt anstellen, bleibt natürlich auch nach Funktionsaufruf bestehen:

Also bekommen Sie als Ausgabe von b.xpos:

9

Merken müssen Sie sich also, dass bei Übergabe von Objekten an Funktionen diese Objekte geändert werden. Sie sollten daher mit Funktionsparametern sehr vorsichtig umgehen und diese nur dann ändern, wenn es notwendig ist.

Objekt als Rückgabewert (Rolle von null)

Es kommt natürlich auch vor, dass eine Funktion ein Objekt zurückgibt. Zum Beispiel, wenn Sie eine Datenbank durchsuchen. Hier soll das erste Auto zurückgegeben werden, das eine bestimmte Mindest-PS-Zahl hat:

Auto findAuto(Auto[] autos, int minPS) {
    for (int i = 0; i  minPS) {
            return autos[i];
        }
    }
    // Problem! Kein return weit und breit...
}

Processing beschwert sich hier. Denn was passiert, wenn ein solches Auto nicht gefunden wird? Processing kommt am Ende der For-Schleife an und findet dort kein return vor. Aber welche Art von Objekt soll denn dann zurückgegeben werden? Was Programmierer in dieser Situation häufig tun, ist, den Wert null zurückzugeben:

Auto findAuto(Auto[] autos, int minPS) {
    for (int i = 0; i  minPS) {
            return autos[i];
        }
    }
    return null
}

Wer auch immer die Funktion verwendet muss also wissen, dass null bedeutet: es wurde nichts gefunden!

Diese Praxis hat einen unangenehmen Nebeneffekt. Wenn Sie eine Funktion verwenden, die potentiell null zurückgibt, müssen Sie aufpassen, wie Sie das zurückgegebene Objekt weiter behandeln:

Auto found = findAuto(meineAutos, 300);
found.printSteckbrief(); // Vorsicht! Könnte null sein.

Es könnte sein, dass in der Variablen found der Wert null steckt. Sie müssen also, bevor Sie die Methode printSteckbrief() aufrufen, sicherstellen, dass found nicht null enthält:

Auto found = findAuto(meineAutos, 300);
if (found != null) {}
    found.printSteckbrief();
}

Solche Abfragen sehen Sie sehr häufig, wenn mit Objekten hantiert wird!

Identität testen

Sie kennen das doppelte Gleichheitszeichen im Zusammenhang mit Zahlen und booleschen Werten. Doch was bedeutet es im Zusammenhang mit Objekten?

Ball b1 = new Ball(10, 20);
Ball b2 = new Ball(10, 20);

if (b1 == b2) {
  println("samesame");
} else {
  println("different");
}
different

Die beiden Ball-Objekte sind zwar "gleich" im dem Sinne, dass sie die gleichen Werte enthalten, aber es handelt sich um zwei unterschiedliche Objekte (= Häuschen). Man kann auch sagen: die beiden Objekte sind nicht identisch.

Merken Sie sich: Das doppelte Gleichheitszeichen testet, ob es sich um dasselbe Häuschen handelt! Anders gesagt: Bei Objekten bedeutet das ==, dass beide Variablen auf dasselbe Objekt zeigen, d.h. es handelt sich um das identische Objekt.

Beachten Sie, dass das nicht immer intuitiv ist, denn es deckt sich nicht mit unserem Verständnis von "Gleichheit". Im folgenden Beispiel erstellen wir zwei Vektoren, die jeweils nur aus Nullen bestehen. Insofern wären beide "gleich". Da es sich aber um zwei verschiedene Objekte handelt, sind sie nicht identisch.

PVector v1 = new PVector();
PVector v2 = new PVector();

println(v1);
println(v2);

if (v1 == v2) {
  println("samesame");
} else {
  println("different");
}
[ 0.0, 0.0, 0.0 ]
[ 0.0, 0.0, 0.0 ]
different

Um Gleichheit zu testen, bieten alle vorhandenen Klassen die Methode equals() an:

PVector v1 = new PVector();
PVector v2 = new PVector();

println(v1);
println(v2);

if (v1.equals(v2)) {
  println("samesame");
} else {
  println("different");
}
[ 0.0, 0.0, 0.0 ]
[ 0.0, 0.0, 0.0 ]
samesame

Das bedeutet: Wenn Sie die Identität von Objekten testen wollen, verwenden Sie ==. Wenn Sie Gleichheit (gleiche Inhalte) testen wollen, verwenden Sie equals().

Bei Strings wollen Sie i.d.R. Gleichheit testen, also verwenden Sie bei Strings immer die Methode equals().

Wenn Sie selbst Klassen schreiben, können Sie die Methode equals() selbst schreiben, um zu definieren, was bei Ihren Klassen Gleichheit genau bedeutet. Wie das funktioniert, erfahren Sie im nächsten Semester.

Übungsaufgaben

(a) Variablen mit Objekten

Schauen Sie sich den folgenden Code an (wir setzen die Klasse Student aus der Aufgabe aus 11.3 voraus):

Student s1 = new Student("Harry", 123);
Student s2 = new Student("Sally", 456);
s2 = s1;
println(s1.name);
println(s2.name);

Bevor Sie das Programm ausprobieren, überlegen Sie, was Sie erwarten. Hatten Sie recht? Warum kommt der Output, den Sie sehen?

Welches Ergebnis hat folgender Code? (Wieder erst überlegen, dann ausführen.)

if (s1 == s2) {
   println("identisch!");
} else {
   println("nicht identisch");
}

(b) Variablen mit Objekten 2

Wir verändern den Code, aber stellen uns ähnliche Fragen:

Student s1 = new Student("Harry", 123);
Student s2 = new Student("Sally", 456);
Student s3 = s1;
s3.name += " Smith";
s2 = s3;
s2.matrikelnummer = 42;
println(s1.name + " " + s1.matrikelnummer);
println(s2.name + " " + s2.matrikelnummer);
println(s3.name + " " + s3.matrikelnummer);

Bevor Sie das Programm ausprobieren, überlegen Sie, was Sie erwarten. Hatten Sie recht? Warum kommt der Output, den Sie sehen?

(c) Gleichheit und Identität

Gegeben sei der Code:

Student s1 = new Student("Harry", 123);
Student s2 = new Student("Harry", 123);

Was kommt raus bei:

if (s1 == s2) {
   println("identisch!");
} else {
   println("nicht identisch");
}

Und warum? Wie würden Sie das Konzept "Gleichheit" testen?

(d) Noten hacken

Gegeben sei folgende Klasse:

class KlausurNote {
  String student;
  int note;

  KlausurNote(String s, int n) {
    student = s;
    note = n;
  }
}

Gegeben sei ferner ein Array mit Noten:

void setup() {
  KlausurNote[] noten = new KlausurNote[3];
  noten[0] = new KlausurNote("Fred", 3);
  noten[1] = new KlausurNote("Susi", 1);
  noten[2] = new KlausurNote("Thore", 2);
}

Drucken Sie alle Namen und Noten auf der Konsole aus:

Fred hat eine 3
Susi hat eine 1
Thore hat eine 2

Schreiben Sie ein Funktion tweakNote mit zwei Parametern: einem Array mit KlausurNote-Objekten und einer Zahl. Die Funktion ändert die Noten aller Objekte im Array zur übergebenen Zahl. Setzen Sie mit Hilfe dieser Funktion alle Noten auf 1.

Testen Sie Ihr Programm, indem Sie nach Aufruf Ihrer Funktion alle Noten noch einmal ausgeben:

Fred hat eine 1
Susi hat eine 1
Thore hat eine 1

(e) Funktionen und Objekte

Gegeben sei die Klasse Auto:

class Auto {
  boolean licht = true;
  boolean motor = true;
  boolean tuev = true;

  Auto(boolean l, boolean m) {
    licht = l;
    motor = m;
  }
}

Schreiben Sie die Funktion tuevCheck, die einen Array von Autos erhält und für jedes Auto prüft, ob Sie derzeit den TÜV erfüllen (hier: Licht und Motor sind true). Falls ja, wird die Variable tuev auf true gesetzt, sonst auf false.

Testen Sie den Code mit:

void setup() {
  Auto[] autos = new Auto[3];
  autos[0] = new Auto(true, false);
  autos[1] = new Auto(false, false);
  autos[2] = new Auto(true, true);
  tuevCheck(autos);
  // Hier alle Autos auf die Konsole ausgeben
}

// Hier Ihre Funktion tuevCheck

11.7 Umgang mit Klassen

Unterschied: Globale Variable vs. Instanzvariable

Sie sollten nie eine globale Variable direkt im Code einer Klasse (d.h. Funktionen oder Konstruktor) verwenden. Beispiel:

// Globale Variable
int x = 0;

class Ball {

  void drawBall() {
   ellipse(x, 100, 10, 10);
  }

  void move() {
    x++; // NICHT GUT!
  }
}

Warum? Weil Ihre Klasse Ball dann nicht unabhängig vom restlichen Code ist. Die Klasse Ball kann also nur in solchen Programmen verwendet werden, wo es eine globale Variable x gibt. Und darüber hinaus muss dieses x auch noch im gleichen Sinne wie im Originalprogramm verwendet werden.

Klassen sollen möglichst autonome Gebilde sein. Man sagt auch, dass eine Klasse eine Art Kapsel ist oder dass die Funktionalität der Klasse gekapselt sein sollte. Das heißt: keine Verbindungen zur Außenwelt, außer durch Parameterübergabe an Funktionen und Konstruktoren.

Wenn wir unser Beispiel verbessern wollen haben wir zwei Möglichkeiten, je nachdem, wie wir unsere Klasse designen wollen.

Möglichkeit 1: Ihre Klasse soll nur zum Zeichnen dienen. Man kann der Klasse sagen "mal einen Ball bei (x, y)". Folglich muss das bewegen des Balls außerhalb der Klasse stattfinden. Jedesmal wenn Sie den Ball zeichnen wollen, müssen Sie die konkreten Koordinaten angeben.

int x = 0;

class Ball {

  void drawBall(int ballX) {
   ellipse(ballX, 100, 10, 10);
  }
}

Möglichkeit 2: Ihre Klasse beinhaltet einen Ball, der sich autonom bewegt, wenn man den entsprechenden Befehl aufruft (move). Folglich muss die Klasse die eigenen Koordinaten kennen und verwalten.

// Hier ist die x-Koordinate in der Klasse gekapselt
class Ball() {
  int x = 0;

  void drawBall(int x) {
   ellipse(x, 100, 10, 10);
  }

  void move() {
    x++;
  }
}

Kein Interfacecode (mousePressed, mouseX, ...) in Ihrer Klasse!

Wenn Sie Klassen für Objekte schreiben, die per Maus manipuliert werden können, müssen Sie davon ausgehen, dass sich das Interface ändert. Vielleicht wollen Sie Ihren Ball per Multitouch, Kinect oder Nintendo-Controller steuern? Dann ist es ungünstig, wenn Sie mit keyCodes arbeiten oder die Variablen mouseX oder mousePressed im Code Ihrer Klasse verwenden.

Stattdessen merken Sie sich bitte, dass jeglicher Interface-Code außerhalb Ihrer Klasse sein sollte.

// Klasse Thing
class Thing {
  int x;
  int y;

  Thing(int ax, int ay) {
    x = ax;
    y = ay;
  }

  void draw() {
    rect(x, y, 20, 20);
  }

  // Nicht gut: Klasse enthält Interface-Code
  // (irgendwas mit key... oder mouse...)

  void checkKeys() {
    if (keyPressed && keyCode == LEFT) {
      x = x - 1;
    }
    if (keyPressed && keyCode == RIGHT) {
      x = x + 1;
    }
  }
}

// Hauptcode
Thing thing = new Thing(50, 50);

void setup() {
  size(100,100);
}

void draw() {
  background(100);
  thing.draw();
  thing.checkKeys();
}

Stattdessen verlagern Sie den Interface-Code in Ihr Hauptprogramm. Sie ergänzen die Klasse eventuell um Methoden zur Bewegung (wie hier moveX), die dann aus dem Hauptprogramm aufgerufen werden.

// Klasse Thing
class Thing {
  int x;
  int y;

  Thing(int ax, int ay) {
    x = ax;
    y = ay;
  }

  void draw() {
    rect(x, y, 20, 20);
  }

  // Bewege entlang x-Achse um Distanz
  void moveX(int distance) {
    x = x + distance;
  }
}

// Hauptcode
Thing thing = new Thing(50, 50);

void setup() {
  size(100, 100);
}

void draw() {
  background(100);
  thing.draw();
  if (keyPressed && keyCode == LEFT) {
    thing.moveX(-1);
  }
  if (keyPressed && keyCode == RIGHT) {
    thing.moveX(1);
  }
}

Sie können sich fürs erste die Regel merken, dass Sie in Ihren Klassen nicht die Variablen mouseX, mouseY, mousePressed, keyPressed, keyCode verwenden sollten.

Klassen gut zu designen ist eine Kunst. Seien Sie also nicht frustriert, wenn Ihnen derzeit nicht klar ist, ob Sie ein Stück Code in die Klasse oder in den Hauptcode setzen sollen.