Behandelte Konzepte/Konstrukte: Klasse, null, Methode, Konstruktor, Punktnotation, Zeiger, Referenz

Lernziele

  • Sie verstehen den Unterschied zwischen Klasse und Objekt (Instanz)
  • Sie können eigene Klassen mit Eigenschaften, Methoden und Konstruktor(en) definieren
  • Sie können Objekte erzeugen und mit diesen arbeiten, also auf Eigenschaften zugreifen und Methoden aufrufen
  • Sie können Klassen erstellen, die selbst Objekte beinhalten
  • Sie können mit Arrays von Objekten umgehen
  • Sie verstehen, wie Objekte in Funktionen modifiziert werden können
  • Sie verstehen die Bedeutung von Zeigern (Referenzen) und Objekte und wie Werte innerhalb von Objekten über Punktnotation verändert werden

Voraussetzungen

Dieses Kapitel baut insbesondere auf der Kenntnis von Objekten auf Kapitel 4.

Sie sollten sicher mit Variablen umgehen können Kapitel 2.

Sie sollten sicher mit If-Anweisungen umgehen können Kapitel 3.

Sie sollten sicher mit Schleifen umgehen können Kapitel 5.

Sie sollten sicher mit Arrays umgehen können Kapitel 6.

Sie sollten sicher mit Funktionen umgehen können Kapitel 7.

Neueste Aktualisierungen (zuletzt 23.01.2024)
  • 23.01.2024: Level zu Aufgaben hinzugefügt
  • 14.12.2021: Neue Aufgabe 8.2j und kleinere Ergänzungen
  • 02.10.2021: Lernziele angepasst
  • 02.08.2021: Neue Kapitelnummerierung
  • 10.01.2021: Beispiel Tabelle (11.2) hinzugefügt
  • 09.01.2021: Box mit Aktualisierungen eingeführt

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

In dem obigen Beispiel werden die Bälle durch eine eigene Klasse repräsentiert, d.h. jeder Ball ist ein Objekt (Aufgabe 8.5e). Außerdem werden hier auch die Verbindungen durch Objekte (und eine Klasse Link) repräsentiert.

8.1 Klassen und Objekte

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.

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

Beispiel: Klasse Person

Ein weiteres Beispiel ist eine Klasse, die eine Person repräsentiert, z.B. für eine Kunden-, Patienten oder Studierenden-Datenbank.

Wir möchten, dass die Klasse Person definieren. Sie soll die Eigenschaften Vorname, Nachname und Geburtsjahr beinhalten.

Wir erzeugen also einen neuen Reiter/Tab "Person" und schreiben:

class Person {
  String vorname;
  String nachname;
  int geburtsjahr;
}

Um Objekte (Instanzen) zu erzeugen, deklarieren wir im Hauptprogramm in setup() zwei neue Variablen.

void setup() {
  Person p1;
  Person p2;
}

Anschließend erzeugen wir die zwei neuen Objekte mit new und weisen sie den Variablen zu:

void setup() {
  Person p1;
  Person p2;
  p1 = new Person();
  p2 = new Person();
}

Das kann man auch verkürzt schreiben:

void setup() {
  Person p1 = new Person();
  Person p2 = new Person();
}

Jetzt können wir die Eigenschaften der zwei Objekte per Punktnotation befüllen:

void setup() {
  Person p1 = new Person();
  Person p2 = new Person();
  p1.vorname = "Johnny";
  p1.nachname = "Depp";
  p1.geburtsjahr = 1963;
  p2.vorname = "Judi";
  p2.nachname = "Dench";
  p2.geburtsjahr = 1934;
}

Auch wenn Sie die Werte auslesen möchten, müssen Sie Punktnotation anwenden:

void setup() {
  Person p1 = new Person();
  Person p2 = new Person();
  p1.vorname = "Johnny";
  p1.nachname = "Depp";
  p1.geburtsjahr = 1963;
  p2.vorname = "Judi";
  p2.nachname = "Dench";
  p2.geburtsjahr = 1934;

  println(p1.nachname + ", " + p1.vorname);
}
Depp, Johnny

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.

Fingerübungen

a) Person

Schreiben Sie ein Programm mit der Klasse Person, die oben im Skript gezeigt wurde. Denken Sie daran, einen Reiter zu erstellen.

class Person {
  String vorname;
  String nachname;
  int geburtsjahr;
}

Im "Hauptprogramm" erstellen Sie eine Variable p und legen dort ein neues Objekt vom Typ Person ab. Befüllen Sie alle Eigenschaften (Instanzvariablen) mit Werten Ihrer Wahl.

Lösung
void setup() {
  Person p = new Person();
  p.vorname = "Harry";
  p.nachname = "Potter";
  p.geburtsjahr = 2010;
}

b) Wohnung

Erstellen Sie eine Klasse Wohnung mit den folgenden Eigenschaften: adresse (Text), qm (Kommazahl), zimmer (ganze Zahl), zuVermieten (Wahrheitswert).

Lösung
class Wohnung {
  String adresse;
  float qm;
  int zimmer;
  boolean zuVermieten;
}

Übungsaufgaben

8.1 a) MyVector   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Sie kennen die Klasse PVector aus Kapitel 4. Versuchen Sie, diese Klasse möglichst genau nachzubauen und nennen Sie Ihre Klasse MyVector. Die Klasse hat zunächst mal nur die zwei Eigenschaften x und y vom Typ float. In den weiteren Abschnitten werden wir die Klasse dann um Methoden und Konstruktoren erweitern.

Um Ihre Klasse zu testen, erzeugen Sie zwei MyVector-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 1: Denken Sie daran, dass Sie, sobald Sie Klassen verwenden, im aktiven Modus sein müssen (setup und/oder draw).

Hinweis 2: 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 oder draw?).

8.1 b) Fehler   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Kopieren Sie folgende Klasse in ein neues Programm:

class Foo {
  int goo;
  boolean isDoo;
}

Im Hauptprogramm dann:

void setup() {
  Foo f1 = new Foo();
  Foo f2;

  f1.goo = 42;
  f2.isDoo = true;
}

Funktioniert der Code? Wenn nicht, warum nicht? Die Fehlermeldung von Processing (Version 3) ist etwas irreführend! Wie reparieren Sie den Code?

Antwort

Es handelt sich eigentlich um eine NullPointerException, denn die Variable enthält den Wert null (siehe oben) und Processing kann daher kein Objekt im Speicher finden, bei dem es die Eigenschaft isDoo modifizieren kann. Processing (3) setzt allerdings noch früher ein und beschwert sich darüber, dass die Variable keinen Initialwert bekommt ("f2 may not have been initialized").

Sie "reparieren" den Code, indem Sie ein "new Foo()" bei f2 einfügen.

8.1 c) Klasse Student   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie eine neue Klasse Student mit zwei Eigenschaften (Instanzvariablen):

  • name (ein String)
  • matrikelnummer (eine 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.

Beachten Sie, dass Klassen immer groß geschrieben werden. Erzeugen Sie auch einen entsprechenden Reiter (Tab) mit exakt dem gleichen Namen wie die Klasse.

8.1 d) Adressbuch   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie eine Klasse, die ein Adressbuch speichern kann. Die Idee ist, dass ein Adressbuch-Objekt viele Adressen speichert.

Die Klasse heißt Adressbuch und hat zwei Instanzvariablen: namen und tel. Beide sind String-Arrays (also nicht nur einfache Strings).

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 < a.namen.length; i++) {
    println(a.namen[i] + ": " + a.tel[i]);
  }
}

Sie sollten sehen:

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

Im Gegensatz zu den vorigen Aufgaben erzeugen Sie hier nur ein Objekt. Neu ist, dass Sie Arrays in der Klasse verwenden.

Zusammenfassung

Eine Klasse ist ein abstrakter Bauplan für konkrete Objekte. Objekte nennt man auch Instanzen. Die Klasse Mensch kann zum Beispiel mehrere Instanzen für konkrete Personen haben.

Eine Klasse definiert Eigenschaften wie name (vom Typ String). Diese Eigenschaften funktionieren wie Variablen, gehören aber zu dieser bestimmten Klasse. Nur ein Objekt hat dann konkrete Werte für diese Eigenschaften, 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 Instanziieren.

Jede Klasse ist gleichzeitig eine Datentyp, d.h. es können Variablen erstellt werden, die dann eine Instanz dieser Klasse speichern können. Eine Variable, die eine Instanz speichert, zeigt nur auf diese Instanz. Man sagt, die Variable hat eine Referenz auf das Objekt. Um zu kennzeichnen, dass eine Variable aktuell auf kein Objekt zeigt, gibt es das Symbol null.

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";
    ...
}

Versucht man die Punktnotation anzuwenden, obwohl die Variable null enthält (also auf gar kein Objekt zeigt), erhält man eine NullPointerException.

8.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 (wie bei Funktionen) 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

Beispiel: Klasse Person

Wir definieren uns für die Klasse Person zunächst mal eine Methode, die den Namen "zusammensetzt" und dann als String zurückgibt:

class Person {
  String vorname;
  String nachname;
  int geburtsjahr;

  String ganzerName() {
    return vorname + " " + nachname;
  }
}

Jetzt können wir die Personen etwas komfortabler benennen:

void setup() {
  Person p1 = new Person();
  Person p2 = new Person();
  p1.vorname = "Johnny";
  p1.nachname = "Depp";
  p1.geburtsjahr = 1963;
  p2.vorname = "Judi";
  p2.nachname = "Dench";
  p2.geburtsjahr = 1934;

  println(p1.ganzerName());
  println(p2.ganzerName());
}
Johnny Depp
Judi Dench

Wir fügen noch zwei weitere Methoden hinzu. Die Methode spreche gibt einen Text aus, schreibt aber zuvor, wer den Text sagt (und benutzt dazu unsere Methode ganzerName()). Die Methode berechneAlter berechnet das aktuelle Alter aufgrund des aktuellen Jahres.

class Person {
  String vorname;
  String nachname;
  int geburtsjahr;

  String ganzerName() {
    return vorname + " " + nachname;
  }

  int berechneAlter(int aktuellesJahr) {
    return aktuellesJahr - geburtsjahr;
  }

  void spreche(String nachricht) {
    println(ganzerName() + " sagt: " + nachricht);
  }
}

Wir verwenden unsere neuen Methoden für einen kleinen Dialog:

void setup() {
  Person p1 = new Person();
  Person p2 = new Person();
  p1.vorname = "Johnny";
  p1.nachname = "Depp";
  p1.geburtsjahr = 1963;
  p2.vorname = "Judi";
  p2.nachname = "Dench";
  p2.geburtsjahr = 1934;

  p1.spreche("Wie geht's dir?");
  p2.spreche("Gut.");
  p1.spreche("Wie alt bist du?");
  p2.spreche("Ich bin " + p2.berechneAlter(2018) + " Jahre alt.");
}
Johnny Depp sagt: Wie geht's dir?
Judi Dench sagt: Gut.
Johnny Depp sagt: Wie alt bist du?
Judi Dench sagt: Ich bin 84 Jahre alt.

Beispiel: Klasse Tabelle

Jetzt sehen wir uns ein weiteres Beispiel an, wo wir eine neue Speichermöglichkeit anlegen. Das nennt man auch eine Datenstruktur. Konkret möchten wir eine Tabelle erzeugen. In einer Tabelle hat man zwei Spalten, eine für die Schlüssel (engl. keys) und eine für die Werte (engl. values).

Der Zweck der Tabelle ist, dass man über den Schlüssel eine bestimmte Information nachschlagen kann. Das klassische Beispiel ist ein Telefonbuch, wo man über den Namen der Person auf die entsprechende Telefonnummer zugreift.

Für unsere Implementierung nehmen wir an, dass Schlüssel und Werte jeweils vom Typ String sind, weil wir damit am flexibelsten sind.

Wir schreiben eine neue Klasse Table. Um die Schlüssel und die Werte zu speichern, verwenden wir String-Arrays. Wir gehen mal von einer maximalen Größe von 100 aus und nutzen eine Variable size, um die Anzahl der benutzten Array-Elemente zu speichern, ähnlich wie bei den flexiblen Arrays.

Hier eine schematische Darstellung der Arrays (mit beispielhaften Werten):

Und jetzt die Klasse:

class Table {
  String[] keys = new String[100];
  String[] values = new String[100];
  int size = 0;

  // Methoden stehen hier
}

Damit man neue Einträge vornehmen kann, schreiben wir die Methode put. Hier werden Schlüssel und Wert übergeben und in die entsprechenden Arrays geschrieben.

void put(String key, String value) {
  keys[size] = key;
  values[size] = value;
  size++;
}

Damit man einen Wert mit Hilfe des Schlüssels auslesen kann, schreiben wir die Methode get. Dort übergibt man den Schlüssel und erhält den entsprechenden Wert als Rückgabe oder auch null, wenn der Schlüssel nicht in der Tabelle vorkommt.

String get(String key) {
  for (int i = 0; i < size; i++) {
    if (keys[i].equals(key)) {
      return values[i];
    }
  }
  return null; // Eintrag nicht vorhanden
}

Hier zeigen wir nochmal die vollständige Klasse, inklusive der Methoden size und printAll, die sicher nützlich sind.

class Table {
  String[] keys = new String[100];
  String[] values = new String[100];
  int size = 0;

  // Größe der Tabelle (befüllter Teil)
  int size() {
    return size;
  }

  // Neues Schlüssel-Wert-Paar eintragen
  void put(String key, String value) {
    keys[size] = key;
    values[size] = value;
    size++;
  }

  // Wert finden
  String get(String key) {
    for (int i = 0; i < size; i++) {
      if (keys[i].equals(key)) {
        return values[i];
      }
    }
    return null; // Eintrag nicht vorhanden
  }

  // Alle Tabelleneinträge ausgeben
  void printAll() {
    for (int i = 0; i < size; i++) {
      println(keys[i] + ": " + values[i]);
    }
  }
}

Und noch ein Beispielprogramm zum Testen:

void setup() {
  Table tab = new Table();

  tab.put("Angela", "111 222");
  tab.put("Joe", "333 444");
  tab.put("Emmanuel", "888 111");

  tab.printAll();

  println();
  println("SUCHE");
  println("Merkel: " + tab.get("Angela"));
  println("Kohl: " + tab.get("Helmut"));
}
Angela: 111 222
Joe: 333 444
Emmanuel: 888 111

SUCHE
Merkel: 111 222
Kohl: null

Was fehlt? Man könnte noch einbauen, dass man Einträge mit einer Methode remove, der man einen Schlüssel übergibt, löschen kann. Vielleicht probieren Sie das mal.

Fingerübungen

a) Vorstellen

Nehmen Sie die Klasse Person aus der Fingerübung des letzten Abschnitts und ergänzen Sie diese um die Methode hallo(). Bei Aufruf, soll die Methoden eine Selbstvorstellung auf der Konsole ausgeben, z.B.

Hallo, mein Name ist Harry Potter.
Lösung
class Person {
  String vorname;
  String nachname;
  int geburtsjahr;

  void hallo() {
    println("Hallo, mein Name ist " + vorname + " " + nachname);
  }
}

Um die Klasse zu testen, nimmt man diesen Code:

void setup() {
  Person p = new Person();
  p.vorname = "Harry";
  p.nachname = "Potter";
  p.geburtsjahr = 2010;
  p.hallo();
}

b) Wohnungsanzeige

Nehmen Sie die Klasse Wohnung aus der Fingerübung des letzten Abschnitts und ergänzen Sie diese um die Methode printAnzeige. Wenn die Wohnung zu vermieten ist, soll erscheinen:

Wohnung zu vermieten: 4 Zimmer, 80.0 qm, Musterstraße 42.

Wenn die Wohnung nicht zur Miete angeboten wird, soll erscheinen:

Wohnung (Musterstraße 42) ist vermietet.

Erstellen Sie ein Objekt und testen Sie Ihre Methode für beide Fälle.

Lösung
class Wohnung {
  String adresse;
  float qm;
  int zimmer;
  boolean zuVermieten;

  void printAnzeige() {
    if (zuVermieten) {
      println("Wohnung zu vermieten: " + zimmer + " Zimmer, "
      + qm + " qm, " + adresse + ".");
    } else {
      println("Wohnung (" + adresse +") ist vermietet.");
    }
  }
}

Übungsaufgaben

8.2 a) MyVector-Methoden   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Nehmen Sie Ihre Klasse MyVector aus Aufgabe 8.1 a). Schreiben Sie die Methoden mult(), add() und sub(), wie Sie sie von PVector aus Kapitel 4 kennen.

Testen Sie den Code, um sicherzustellen, dass alle Methoden korrekt funktionieren. Schauen Sie am besten nochmal in Kapitel 4 nach, wie die drei Methoden genau funktioneren sollten.

8.2 b) Sprechmaschine   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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.

8.2 c) Losmaschine   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Gegeben sei die folgende Klasse:

class Losmaschine {
  String[] lose = { "Niete", "Niete", "1 Punkt", "Niete", "1 Punkt", "10 Punkte" };
}

Schreiben Sie eine Methode losZiehen (ohne Parameter), die ein zufällig gewähltes Los zurückgibt (String).

Testen Sie Ihre Code, indem Sie folgenden Code mehrfach laufen lassen:

void setup() {
  Losmaschine lm = new Losmaschine();
  println(lm.losZiehen());
}

Alternativ können Sie auch das Losziehen in eine Schleife mit ein paar Durchläufen packen.

8.2 d) Klasse Tropfen   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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. Interessanter wird es, wenn die Tropfen unterschiedliche y-Positionen haben.

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

8.2 e) Farbige Tropfen   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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.

Zusätzlich soll jedesmal, wenn die Tropfen unten verschwinden bzw. oben neu erscheinen, eine neue Farbe gewählt werden. Nutzen Sie dazu auf jeden Fall die Methode neueFarbe().

8.2 f) Student-Methode   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Diese Aufgabe baut auf einer vorherigen Aufgabe auf.

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

8.2 g) Auto   Level 41 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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.

8.2 h) Adressbuch-Suche   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Diese Aufgabe baut auf Aufgabe 8.1 d) auf.

Verwenden Sie die Klasse Adressbuch und erweitern Sie diese um die Methode findeTel. Diese Methode bekommt als Parameter 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

8.2 i) Zahlenliste   Level 51 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Arrays haben den Nachteil, dass man sie zur Laufzeit nicht verlängern kann. Daher haben wir uns das Konzept der flexiblen Arrays angeschaut. Wir wollen einen flexiblen Array von Zahlen jetzt in eine Klasse packen, so dass wir das Konzept komfortabel benutzen können. Man nennt das auch eine Liste.

Schreiben Sie eine Klasse Zahlenliste. Die Klasse soll einen Integer-Array der Länge 1000 beinhalten und sich in einer weiteren Variable merken, wie viele der Zellen wir wirklich nutzen (zu Beginn 0).

Die Klasse soll folgende Methoden haben:

  • addZahl: bekommt eine Integer-Zahl und fügt sie dem Array hinzu.
  • getZahl: bekommt einen Index und gibt die entsprechend gespeicherte Zahl zurück (z.B. für Index 2 die dritte Zahl). Wenn der Index ungültig ist (zu hoch oder negativ), wird eine 0 zurückgegeben.
  • size: gibt die Anzahl der gespeicherten Zahlen zurück.
  • show: schreibt die Zahlenliste auf die Konsole (siehe Beispiel unten).
void setup() {
  Zahlenliste liste = new Zahlenliste();
  liste.addZahl(10);
  liste.addZahl(20);
  liste.addZahl(30);
  println("Liste der Länge " + liste.size());
  liste.show();

  // Liste verlängern und nochmal ausgeben
  liste.addZahl(-5);
  liste.addZahl(1002);
  println("Liste der Länge " + liste.size());
  liste.show();
}
Sie sollten sehen:
Liste der Länge 3
[0] 10
[1] 20
[2] 30
Liste der Länge 5
[0] 10
[1] 20
[2] 30
[3] -5
[4] 1002

8.2 j) Zahlenliste ohne Limits   Level 51 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Die obige Aufgabe arbeitet mit einer bestimmten Kapazität von 1000 Elementen, die die Liste speichern könnte. Was passiert, wenn man das 1001-ste Element hinzufügt? Offensichtlich haben Sie dann ein Problem.

Die Lösung ist, einen neuen Array zu erstellen, z.B. mit doppelter Länge (= Kapazität) als vorher, dort die vorhandenen Elemente hineinzukopieren und die Array-Instanzvariable auf den neuen Array gesetzt wird. Das könnte man in einer neuen Methode extend() durchführen.

Die Aufgabe ist also, einen solchen Mechanismus einzubauen. Setzen Sie zum Testen die Länge des internen Arrays auf 5 (das nennen wir die Kapazität) und testen Sie Ihren Code mit

void setup() {
  Zahlenliste liste = new Zahlenliste();
  liste.addZahl(10);
  liste.addZahl(20);
  liste.addZahl(30);
  liste.addZahl(22);
  liste.addZahl(-5);
  liste.addZahl(1002);
  liste.show();
}

Wenn Sie möchten, können Sie sinnvolle Infotexte ausgeben. Dann könnte die Ausgabe so aussehen:

adding 10
capacity: 5, size: 1, free: 4
adding 20
capacity: 5, size: 2, free: 3
adding 30
capacity: 5, size: 3, free: 2
adding 22
capacity: 5, size: 4, free: 1
adding -5
capacity: 5, size: 5, free: 0
adding 1002
extending capacity to 10
capacity: 10, size: 6, free: 4
[0] 10
[1] 20
[2] 30
[3] 22
[4] -5
[5] 1002  

Zusammenfassung

Klassen können neben Eigenschaften auch Funktionen beinhalten, man nennt Funktionen in Klassen Methoden. Methoden definieren Aktionen, die zu konkreten Objekten einer Klasse gehören. Zum Beispiel: das Zeichnen eines Objekts an einer bestimmten Position oder eine mathematische Operation auf einem bestimmten Vektor.

Methoden werden genauso wie Funktionen definiert, allerdings innerhalb der Klasse, zu der sie gehören:

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

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

Der Aufruf einer Methode erfolgt über Punktnotation:

Ball ball = new Ball();
ball.zeichne();

8.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, wenn Sie keine eigenen Konstruktoren erzeugen. Er enthält aber keine besondere Funktionalität.

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 weiteren Methoden
}

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

Sie können im Konstruktor beliebigen Code unterbringen. Zum Beispiel können Sie eine kurze Ausgabe einbauen, die signalisiert, dass der Konstruktor aufgerufen wurde.

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

   // Ihr eigener Konstruktor
   Ball()
   {
      xpos = 50;
      ypos = 50;
      durchmesser = 20;
      println("Neuen Ball erzeugt.");
   }

   // hier kommen die weitere Methoden
}

Wenn Sie jetzt zwei Ball-Objekte erzeugen:

void setup() {
  Ball b1 = new Ball();
  Ball b2 = new Ball();
}

Sehen Sie:

Neuen Ball erzeugt.
Neuen Ball erzeugt.

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:

void setup() {
  Ball ball = new Ball(70, 60, 30);
}

Sobald Sie einen eigenen Konstruktor definieren, wird der leere Konstruktor nicht mehr automatisch erzeugt.

Das hat zur Folge, dass dieser Code jetzt fehlerhaft ist:

void setup() {
  Ball ball = new Ball(70, 60, 30);
  Ball ball2 = new Ball(); // FEHLER: Konstruktor fehlt!
}

Sie können auch 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;
      println("Konstruktor 1 aufgerufen.");
   }

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

Dann könnten Sie folgendermaßen Instanziieren:

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

Unsere Konsole zeigt:

Konstruktor 2 aufgerufen mit 70, 60, 30
Konstruktor 1 aufgerufen.
Konstruktor 1 aufgerufen.

Beispiel: Klasse Person

Auch für unsere Klasse Person lässt sich ein sinnvoller Konstruktor definieren.

class Person {
  String vorname;
  String nachname;
  int geburtsjahr;

  Person(String vn, String nn, int gj) {
    vorname = vn;
    nachname = nn;
    geburtsjahr = gj;
  }
}

Jetzt können wir unsere Objekte mit wesentlich weniger Code erzeugen:

void setup() {
  Person p1 = new Person("Jonny", "Depp", 1963);
  Person p2 = new Person("Judi", "Dench", 1934);
}

Fingerübungen

a) Konstruktor für den Tropfen

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

class Tropfen {
  float x;
  float y;
}
Lösung
class Tropfen {
  float x;
  float y;

  Tropfen (float px, float py) {
    x = px;
    y = py;
  }
}

Übungsaufgaben

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

8.3 a) MyVector-Konstruktoren   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Fügen Sie Iherer Klasse MyVector aus Aufgaben 8.1 a) und 8.1 b) zwei Konstruktoren hinzu. Zunächst einen Konstruktor mit zwei Parametern für x und y. Dann den leeren Konstruktor, der einen Vektor mit Werten 0 erstellt.

Testen Sie beide Konstruktoren mit entsprechenden Aufrufen.

8.3 b) Leerer Konstruktor für den Tropfen   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie einen zusätzlichen Konstruktor für die Klasse Tropfen aus der Fingerübung, 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.

8.3 c) Student-Konstruktor   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Diese Aufgabe baut auf einer vorherigen Aufgabe auf.

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.

8.3 d) Konstruktor mit Funktionalität   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie die Klasse Kunde mit folgenden Eigenschaften:

  • name: ein einziger String, der den kompletten Namen (inkl. Titel) enthält
  • kundennr: eine ganze Zahl

Schreiben Sie zwei Konstruktoren:

  1. Der erste Konstruktor bekommt zwei Strings (vorname, nachname) und eine Zahl (nr). Setzen Sie im Kontruktor die Strings für die Eigenschaft name zusammen!
  2. Der zweite Konstruktor hat zusätzlich den Parameter "titel". Auch hier soll der Name korrekt zusammengesetzt werden.

Testen Sie Ihren Code mit:

void setup() {
  Kunde k1 = new Kunde("Haribert", "Schmidt", 501);
  Kunde k2 = new Kunde("Prof. Dr. Dr.", "Lara", "Schulte", 339);

  println(k1.name);
  println(k2.name);
}

8.3 e) Konstruktor mit Stringzerlegung   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie die Klasse Kunde mit folgenden Eigenschaften:

  • vorname: ein String
  • nachname: ein String
  • kundennr: eine ganze Zahl

Schreiben Sie einen Konstruktor mit den Parametern name (String) und nr (int). In diesem Konstruktor müssen Sie den Namen "auseinander nehmen", um die Eigenschaften "vorname" und "nachname" zu füllen. Verwenden Sie dazu die String-Methoden indexOf und substring, die Sie aus Kap. 6.2 kennen.

Sie können davon ausgehen, dass der übergebene Name lediglich einen Vornamen und einen Nachnamen und keine Titel etc. enthält.

Testen Sie Ihren Code mit:

void setup() {
  Kunde k1 = new Kunde("Haribert Schmidt", 501);
  Kunde k2 = new Kunde("Lara Schulte", 339);

  println(k1.vorname);
  println(k1.nachname);
  println(k2.vorname);
  println(k2.nachname);
}

8.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 auch, 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.

Grafisches Beispiel

Wir schauen uns ein weiteres Beispiel aus der Computergrafik an, wo Objekte miteinander verknüpft sind.

Zunächst definieren wir eine Klasse Ball, die einen Ball so animiert, dass er von den Wänden abprallt:

class Ball {
  PVector pos;
  PVector speed;

  Ball() {
    pos = new PVector(random(5, width-5), random(5, height-5));
    speed = new PVector(random(-2, 2), random(-2,2));
  }

  void render() {
    noStroke();
    ellipse(pos.x, pos.y, 10, 10);
  }

  void update() {
    pos.add(speed);
    if (pos.x < 5 || pos.x > width-5) {
      speed.x = -speed.x;
    }
    if (pos.y < 5 || pos.y > height-5) {
      speed.y = -speed.y;
    }
  }
}

Wir animieren im Hauptprogramm drei Ball-Objekte:

Ball b1 = new Ball();
Ball b2 = new Ball();
Ball b3 = new Ball();

void draw() {
  background(0);

  b1.render();
  b2.render();
  b3.render();

  b1.update();
  b2.update();
  b3.update();
}

Jetzt möchten wir Verbindungen zwischen je zwei Bällen hinzufügen. Jede Verbindung kann man sich als Objekt vorstellen, wo jeweils die zwei "Enden" gespeichert sind. Wir nennen die dazu notwendige Klasse Link.

class Link {
  Ball ball1;
  Ball ball2;

  Link(Ball b1, Ball b2) {
    ball1 = b1;
    ball2 = b2;
  }

  void render() {
    stroke(255);
    line(ball1.pos.x, ball1.pos.y, ball2.pos.x, ball2.pos.y);
  }
}

In der Methode render() sehen Sie wieder ein Beispiel für verkettete Punktnotation.

In unserem Hauptprogramm bauen wir zwei Links ein:

Ball b1 = new Ball();
Ball b2 = new Ball();
Ball b3 = new Ball();
Link link1 = new Link(b1, b2);
Link link2 = new Link(b2, b3);

void draw() {
  background(0);

  link1.render();
  link2.render();

  b1.render();
  b2.render();
  b3.render();

  b1.update();
  b2.update();
  b3.update();
}

Ein Objekt kann also auch eine Beziehung zwischen zwei oder mehr Objekten sein. Dass Objekte aufeinander verweisen (= zeigen), ist in der objektorientierten Programmierung ganz normal.

Übungsaufgaben

8.4 a) Punktnotation   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Gegeben seien die zwei Klassen Auto und Kunde (s.o.) und der folgende 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.

8.4 b) Funktion   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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

8.4 c) Freunde   Level 41 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Im Rahmen eines Sozialen-Netzwerk-Projekts, haben Sie eine Klasse Member:

class Member {
  String name;

  Member(String n) {
    name = n;
  }
}

Sie haben auch Code zum Testen:

void setup() {
  Member a = new Member("Hans");
  Member b = new Member("Anna");
  Member c = new Member("Franz");
  Member d = new Member("Julia");

  println(a.name + " ist Mitglied.");
  println(b.name + " ist Mitglied.");
  println(c.name + " ist Mitglied.");
  println(d.name + " ist Mitglied.");
}

Erweitern Sie die Klasse um eine Eigenschaft friend, die eine/n Freund/in speichern kann (als Objekt). Schreiben Sie außerdem eine Methode getFriendName, die einen String zurückgibt, nämlich entweder den Namen des Freundes oder "niemand", falls kein Freund vorhanden ist.

Erzeugen Sie im Testcode (setup) Freundschaften zwischen Hans-Anna und Julia-Franz. Sorgen Sie dafür, dass die Freundschaften in beide Richtungen verlaufen. Ersetzen Sie die Print-Anweisungen durch eine Ausgabe, die für jedes Mitglied den/die entsprechende Freund/in ausgibt. Sie sollten folgende Ausgabe sehen (nutzen Sie die Methode getFriendName):

Freund von Hans ist Anna
Freund von Anna ist Hans
Freund von Franz ist Julia
Freund von Julia ist Franz

In einem zweiten Schritt erweitern Sie die Klasse nochmals um eine Methode setFriend, die dafür sorgt, dass Sie Freundschaftszuweisungen (a.friend = b) nur einmal durchführen müssen (d.h. Sie sparen sich b.friend = a). Passen Sie Ihren Testcode entsprechend an (Sie sollten zwei Zeilen weniger Code haben) und testen Sie. Für diesen Schritt benötigen Sie das Schlüsselwort this. Dieses repräsentiert innerhalb einer Klasse das aktuelle, eigene Objekt.

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

8.5 a) Ausgabe und Filtern   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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

8.5 b) Filtern mit Funktion   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie eine Funktion printByAge, die zwei Parameter bekommt: einen Personenarray (siehe oben) 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!

8.5 c) Filtern als Funktion mit Rückgabe   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie eine Funktion getByAge, die zwei Parameter bekommt: einen Personenarray (siehe oben) 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).

8.5 d) Viele Kisten   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie die Klasse Kiste mit den Eigenschaften x, y, breite und hoehe. Schreiben Sie einen entsprechenden Konstruktor und die Methode zeichne(), die an der entsprechenden Stelle ein Rechteck zeichnet.

Erzeugen Sie einen Array von 10 Objekten mit zufälliger Position und zufälligen Werten für Breite und Höhe (z.B. zwischen 10 und 40). Rufen Sie für alle Objekte die Methode zeichne() auf.

8.5 e) Netz von Bällen   Level 51 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Sie sollen eine Menge Bälle (z.B. 15) animieren. Jedesmal, wenn zwei Bälle kollidieren, soll eine Verbindung (Link) zwischen den zwei Bällen entstehen.

Schauen Sie sich das grafische Beispiel aus dem Abschnitt "Interaktion zwischen Objekten" an. Jetzt erstellen Sie einen Array von z.B. 20 Bällen. Zustätzlich brauchen Sie einen Array für die Links. Aber der Array der Links sollte flexibel sein (zu Beginn enthält er keine Links). Schauen Sie nochmal am Ende von Kapitel "Arrays", wie das geht.

In dem folgenden Beispielfenster sind die Bälle und Verbindungen zufällig in unterschiedlichen Graustufen eingefärbt.


(Wenn bei Ihnen die meisten Bälle bereits verbunden sind, lösen Sie ein "Reload" auf der Seite auf, um zu sehen, wie die Bälle ohne Verbindungen starten.)

Tipp
Für die Kollisionskontrolle schreiben Sie in der Klasse Ball eine Methode checkCollision(Ball[] balls), die den Array aller Bälle bekommt und das Ball-Objekt zurückgibt, mit dem der Ball kollidiert (oder null). Im Hauptprogramm rufen Sie die Methode für alle Bälle auf und erzeugen einen neuen Link, wenn ein Ball zurückgegeben wird.

8.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 Ball {
   int xpos;
   int ypos;

   Ball(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 < autos.length; i++) {
        if (autos[i].ps > 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 < autos.length; i++) {
        if (autos[i].ps > 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

8.6 a) Variablen mit Objekten   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Diese Aufgabe setzt die Klasse Student aus einer vorherigen Aufgabe voraus.

Schauen Sie sich den folgenden Code an:

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");
}

8.6 b) Variablen mit Objekten 2   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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?

8.6 c) Gleichheit und Identität   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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?

8.6 d) Noten manipulieren   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

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

8.6 e) Funktionen und Objekte   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Gegeben sei die folgende 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

8.7 Umgang mit Klassen

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. Hier zeigen wir ein paar Situationen, wo man klare Empfehlungen geben kann.

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++;
  }
}

Möglichkeit 2 ist in der Regel zu bevorzugen, d.h. versuchen Sie immer, alle relevanten Informationen in der Klasse zu halten.

Kein Interfacecode 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 am besten, 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.