Behandelte Konzepte/Konstrukte: PVector, new, set(), mult(), add(), dist(), equals(), String, length()

Lernziele

  • Sie können die Unterschiede zwischen einem primitiven Datentyp und einem Objekttyp (Klasse) anhand der Begriffe Referenz bzw. Zeiger erklären
  • Sie können (Vektor-)Objekte erzeugen und in entsprechenden Variablen speichern
  • Sie können mit Punktnotation auf Eigenschaften und Methoden von Objekten zugreifen
  • Sie kennen den Unterschied zwischen Identität (das selbe Objekt) und Gleichheit (z.B. gleiche Inhalte) und können dies mit Beispielen erläutern
  • Sie wissen, wie man Gleichheit von zwei Objekten (z.B. Vektoren, Strings) prüft
  • Sie können Referenzen auf Objekte im Code so verfolgen, dass klar ist, welches Objekt welche Werte hat
  • Sie können mit Vektorobjekten programmieren, z.B. rechnen oder animieren
  • Sie verstehen, dass Strings Objekte sind, und erkennen Eigenschaften und Methoden von Strings als Mechanismen von Objekten

Voraussetzungen

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

Um Beispiele und Übungsaufgaben zu verstehen, müssen Sie auch If-Anweisungen verstanden haben Kapitel 3.

Neueste Aktualisierungen (zuletzt 11.01.2024)
  • 11.01.2024: Level zu Aufgaben hinzugefügt
  • 15.11.2023: Neues Video in 4.2 "Methoden von Strings"
  • 11.11.2023: Kleinigkeiten im Kap. Strings angepasst
  • 05.12.2022: String-Fingerübungen ergänzt
  • 01.12.2022: String-Pool erklärt (4.2)
  • 03.10.2021: Lernziele angepasst
  • 02.08.2021: Neue Kapitelnummerierung

Bisher haben wir Variablen verwendet, um sehr einfache Informationen zu speichern: Ganze Zahlen (int), Dezimalzahlen (float), einzelne Zeichen (char) oder Wahrheitswerte (boolean). Diese Datentypen nennt man primitive Datentypen.

Primitiver Datentyp

Zu den primitive Datentypen gehören unter anderem die Typen float, int, char und boolean (alle Datentypen, die in Processing/Java klein geschrieben werden). Variablen eines primitiven Datentyps beinhalten Werte, wie z.B. eine ganze Zahl im Fall von int.

Jetzt möchte man häufig mehrere zusammengehörige Informationen in einem Container bündeln, z.B. den x- und y-Wert eines Vektors oder die unterschiedlichen Informationen zu einem Kunden (Vorname, Nachname, Mailadresse etc.).

In diesem Kapitel lernen Sie Objekte kennen, in denen man mehrere Informationen speichern kann. Es geht in diesem Kapitel nur um die Benutzung solcher Objekte. Wie man eigene Objekt-Typen, sogenannte Klassen, herstellt, lernen wir in einem späteren Kapitel. Ein Objekt ist ein Konstrukt irgendwo im Speicher, das mehrere Daten speichern kann:

Stellen Sie sich vor, Sie haben mehrere Spielfiguren in einem Computerspiel. Ihr Programm würde jede Figur in einem Objekt speichern, das bestimmte Eigenschaften hat, zum Beispiel die aktuellen (x, y)-Koordinaten, die Lebensenergie, die Besitztümern etc. Außerdem möchten Sie, dass die Figuren bestimmte Aktionen durchführen kann, z.B. springen, einen Schuss abgeben, einen Gegenstand an sich nehmen. Diese Aktionen müssen natürlich an individuelle Figuren (Objekte) gebunden sein. Sonst würde die falsche Figur springen oder schießen.

Objekt

Ein Objekt ist eine Speicherstruktur, die sowohl mehrere Eigenschaften (ähnlich zu Variablen) als auch Aktionen (ähnlich zu Funktionen) beinhaltet. Diese Aktionen werden auch Methoden genannt.

Wir beginnen aber mit einem sehr einfachen Objekt: einem Vektor.

4.1 Vektoren

Es gibt viele verschiedene Typen von Objekten, später werden Sie diese selbst programmieren. Einen bestimmten Typ von Objekten nennt man auch einen Objekttyp oder eine Klasse (engl. class). Die Klasse ist sozusagen der Bauplan einer Sorte von Objekten.

Für Vektoren gibt es die Klasse PVector, für Zeichenketten die Klasse String, für Digitalbilder die Klasse PImage usw. Jede Klasse ist gleichzeitig auch ein Datentyp. Um den Unterschied zu primitiven Typen zu kennzeichnen, nennt man eine Klasse auch einen Objekttyp.

Objektyp/Klasse

Ein Objekttyp wird in Java groß geschrieben. Beispiele sind PVector (nur Processing) und String. Ein Objekttyp ist immer auch eine Klasse. Eine Variable eines Objekttyps kann Objekte der entsprechenden Klasse speichern. Die Variable enthält dabei als Wert eine Adresse, d.h. die Variable "zeigt" auf das Objekt. Man sagt auch, die Variable enthält eine Referenz oder einen Pointer.

Wir schauen uns jetzt die Klasse PVector an.

Objekt erzeugen

Ein Vektor hat eine x- und eine y-Komponente, das sind die Eigenschaften eines jeden Objekts. Wir wollen einen neuen Vektor mit den Werten (4, 2) erstellen. Dazu definieren wir zunächst eine neue Variable vec vom Typ PVector:

PVector vec; // kann einen Vektor speichern

Sie sehen: das funktioniert genauso wie bei einer int-Variablen.

int foo; // kann eine ganze Zahl speichern

Subtiler Unterschied: PVector wird groß geschrieben. Die Großschreibung weist darauf hin, dass wir es nicht mit einem primitiven Datentypen, sondern mit einem Objekttypen bzw. einer Klasse zu tun haben.

Variablen zeigen auf Objekte

Wir lernen jetzt einen wichtigen Unterschied zwischen primitiven Datentypen und Objekttypen kennen. Dieser Unterschied betrifft die die Rolle der Variablen. Im Beispiel oben ist unsere Variable vec ist zunächst "leer". Im Gegensatz zu einer int-Variable, die zu Beginn immer 0 ist, enthält unsere Variable das Stichwort null. Das bedeutet, dass die Variable derzeit "auf kein Objekt zeigt". Wir haben ja auch noch gar kein Objekt hergestellt. Also sieht die Situation wie folgt aus:

Jetzt erzeugen wir das neue Vektorobjekt, das in vec gespeichert wird. Dazu schreiben wir das Schlüsselwort new, dann den Namen der Klasse und dann einige Parameter in Klammern. Die Art und Anzahl dieser Parameter kann variieren, bei PVector haben wir zwei float-Parameter für x und y:

vec = new PVector(4, 2); // neuer Vektor mit x=4 und y=2

Hier passieren zwei Dinge. Erstens wird mit new PVector(4, 2) ein neues Objekt im Speicher angelegt.

Im nächsten Schritt wird in der Variablen vec eine Referenz auf das Objekt angelegt. Das kann man sich als Pfeil oder Zeiger in den Speicher vorstellen. Deshalb sagt man auch: Die Variable "zeigt" auf das Objekt.

Natürlich kann man Variablendeklaration und Zuweisung wie gewohnt auf einer Zeile durchführen:

PVector vec = new PVector(4, 2);

Wenn Sie den Nullvektor (0, 0) haben möchten, können Sie auch die Parameter weglassen, nur die Klammern müssen Sie unbedingt hinschreiben.

PVector vec = new PVector(); // Nullvektor

Eigenschaften

Wir haben jetzt eine neue Variable vec vom Typ PVector. Wie arbeiten wir jetzt mit dieser Variablen?

Wir können einerseits Eigenschaften auslesen (hier wären das x und y), indem wir den Variablennamen, dann einen Punkt, dann die Eigenschaft hinschreiben:

println(vec.x);
println(vec.y);

Dies nennt man auch die Punktnotation und erlaubt es uns, zwischen den Eigenschaften verschiedener Objekten zu unterscheiden. Definieren wir ein zweites Vektorobjekt in vec2...

PVector vec2 = new PVector(10, -30);

Schematisch kann man sich das so vorstellen:

...dann möchten Sie natürlich zwischen den x/y-Koordination von vec und vec2 unterscheiden:

println(vec.x);
println(vec.y);
println(vec2.x);
println(vec2.y);

Sie können sich die Objekte jeweils in ihrer Gesamtheit mit println() betrachten:

PVector vec = new PVector(4, 2);
PVector vec2 = new PVector(10, -30);
println(vec);
println(vec2);
[ 4.0, 2.0, 0.0 ]
[ 10.0, -30.0, 0.0 ]

Sie sehen, dass PVector eigentlich ein 3D-Vektor ist, also die Eigenschaften x, y und z hat. Ignorieren Sie einfach die z-Komponente, wenn Sie in 2D arbeiten.

Sie können auch die Eigenschaften durch Zuweisung verändern, genauso wie normale Variablen:

PVector v = new PVector(10, 20);

println(v);

v.x = 200;
v.y = 1;

println(v);
[ 10.0, 20.0, 0.0 ]
[ 200.0, 1.0, 0.0 ]

Schauen wir uns gleich ein Beispiel an, wo wir Vektoren verwenden. Wir lassen wieder mal einen Ball fliegen und speichern die aktuelle x/y-Position in einem Vektor:

PVector pos;

void setup() {
  pos = new PVector(); // neuer Null-Vektor
}

void draw() {
  background(255);
  ellipse(pos.x, pos.y, 20, 20); // Werte auslesen
  pos.x++; // Werte erhöhen
  pos.y++;
}

Der Ball fliegt von links oben diagonal nach rechts unten. Beachten Sie, dass Sie die Eigenschaften pos.x und pos.y wie ganz normale Variablen behandeln können (hier mit dem Inkrement-Operator ++).

Eine kürzere Variante ohne setup():

PVector pos = new PVector();

void draw() {
  background(255);
  ellipse(pos.x, pos.y, 20, 20);
  pos.x++;
  pos.y++;
}

Sie sehen schon einen kleinen Vorteil gegenüber der "alten Methode" (zwei Variablen x und y verwenden), wenn Sie einen zweiten Ball verwenden:

PVector pos1 = new PVector(0, 0); // links oben
PVector pos2 = new PVector(100, 0); // rechts oben

void draw() {
  background(255);
  ellipse(pos1.x, pos1.y, 20, 20);
  ellipse(pos2.x, pos2.y, 20, 20);

  pos1.x++; // von links nach rechts
  pos1.y++;
  pos2.x--; // von rechts nach links
  pos2.y++;
}

Mit der alten Methode hätten Sie hier bereits 4 Variablen (x1, y1, x2, y2). Die Objekte erlauben eine kompaktere Darstellung.

Methoden

Objekte können nicht nur mehr Informationen speichern als Variablen, sie bieten darüber hinaus auch Aktionen an, die etwas "tun", also etwas berechnen oder bewegen.

Ein Vektor kann z.B. seine Länge berechnen:

float l = vec.mag();

Die Methode mag() (für engl. magnitude) berechnet die Länge des Vektors und gibt diese zurück. Beachten Sie die Punktnotation und die Klammern!

Um die Eigenschaften x und y des Vektors zu ändern, können Sie die Methode set() verwenden:

PVector vec = new PVector();
vec.set(0, 50); // setzt x=0 y=50

Die Methode mult() multipliziert den gesamten Vektor (also sowohl x als auch y) mit einer Zahl. Diese Methode ändert also die Eigenschaften des Objekts!

PVector vec = new PVector(2, 3);

println("vorher:" + vec);
vec.mult(10);
println("nachher: " + vec);
vorher:[ 2.0, 3.0, 0.0 ]
nachher: [ 20.0, 30.0, 0.0 ]

Die Methode add() addiert einen anderen Vektor zu dem eigenen Vektor hinzu. Im folgenden Code wird vec also wieder verändert:

PVector vec = new PVector(1, 1);
PVector vec2 = new PVector(10, 10);

vec.add(vec2);

println(vec);
[ 11.0, 11.0, 0.0 ]

Ähnlich funktionieren die Methoden sub() (Subtraktion), dot() (Skalar- oder Punktprodukt) und cross() (Kreuz- oder Vektorprodukt). Jedesmal wird der Vektor, auf dem Sie die Methode aufrufen, verändert.

Sicherlich noch von Interesse ist die Methode dist(), welche die Distanz zwischen zwei Punkten misst (hier wird ein Vektor als Punkt x, y im Raum interpretiert):

PVector v1 = new PVector(1,1);
PVector v2 = new PVector(5,1);
float d = v1.dist(v2);

println(d);
4.0

In diesem Fall wird v1 natürlich nicht verändert. Und richtig, man könnte auch schreiben v2.dist(v1).

Identität und Gleichheit

Nehmen wir an, Sie haben zwei Vektoren v und w:

PVector v = new PVector(5, 0);
PVector w = new PVector(5, 0);

Im Speicher sieht das so aus:

Sind die beiden Vektoren "gleich"? Bei int-Variablen prüfen Sie Gleichheit mit dem doppelten Gleichheitszeichen. Lassen Sie uns das versuchen:

println(v == w);
false

Offensichtlich findet Processing nicht, dass v und w gleich sind. Warum ist das so? Weil v und w zwei unterschiedliche Objekte sind, wie man im Bild des Speichers oben klar sieht.

Das == prüft also, ob zwei Variablen das selbe Objekt beinhalten. Man kann auch sagen: Das == prüft die Identität.

Möchten wir prüfen, ob zwei Vektoren die gleichen Werte haben, können wir die PVector-Methode equals() verwenden:

println(v.equals(w));
true

Sie müssen bei Objekten also immer unterscheiden zwischen

  • Identität: Zwei Variablen beinhalten das selbe Objekt. Man sagt auch: zwei Variablen "zeigen" auf das selbe Objekt. Dies wird mit == geprüft.
  • Gleichheit: Zwei Variablen zeigen auf Objekte, die die gleichen Eigenschaften haben. Bei Vektoren heißt das: zwei Vektorobjekte haben die gleichen x/y-Werte. Dies wird mit der Methode equals() geprüft.

Im nächsten Abschnitt über Strings und in Kap. 11 werden Sie mehr dazu hören.

Anwendungsbeispiel

Wenn wir einen Ball animieren, brauchen wir zunächst seine Position (zunächst nur x) und eine Geschwindigkeit (xspeed):

float x = 0;
float xspeed = 1;

void draw() {
  background(255);
  ellipse(x, 50, 20, 20);
  x = x + xspeed;

  if (x > width) {
    x = 0;
  }
}

Jetzt wollen wir den Ball diagonal fliegen lassen und führen dazu die Variablen y und yspeed ein:

float x = 0;
float y = 0;
float xspeed = 1;
float yspeed = 1;

void draw() {
  background(255);
  ellipse(x, y, 20, 20);
  x = x + xspeed;
  y = y + yspeed;

  if (x > width) {
    x = 0;
  }

  if (y > height) {
    y = 0;
  }
}

Beachten Sie, dass Sie für xspeed und yspeed auch andere Werte einsetzen könnten. Genau betrachtet handelt es sich um einen Vektor, der die Richtung des Balls bestimmt. Probieren Sie z.B. xspeed = 2 und yspeed = 3:

Sowohl die Position des Balls (x, y) als auch die Geschwindigkeit (xspeed, yspeed) sind Vektoren. Also nutzen wir unsere neue Klasse PVector!

PVector pos = new PVector(); // Nullvektor
PVector speed = new PVector(3,2);

void draw() {
  background(255);
  ellipse(pos.x, pos.y, 20, 20);

    pos.add(speed);

  if (pos.x > width) {
    pos.x = 0;
  }

  if (pos.y > height) {
    pos.y = 0;
  }
}

Fingerübungen

a) Vektoren erzeugen

Erzeugen Sie zwei Variablen a und b für Vektoren.

Variable a soll den Vektor (11, -2) enthalten, Variable b den Vektor (5, 4).

Lösung
PVector a = new PVector(11, -2);
PVector b = new PVector(5, 4);

b) Eigenschaften ausgeben

Ergänzen Sie den Code aus der obigen Übung. Geben Sie die x-Koordinate von a und die y-Koordinate von b auf der Konsole aus.

Lösung
println(a.x);
println(b.y);

c) Vektoren vergleichen

Nehmen Sie die Vektoren a und b von oben. Erzeugen Sie zwei weitere Variablen c und d. Variable c soll auf denselben Vektor zeigen wie a. Variable d soll einen neuen Vektor mit Werten (11, -2) enthalten.

Prüfen Sie c und a auf Identität und auf Gleichheit. Überlegen Sie vor Programmstart, was herauskommen sollte.

Tun Sie das gleiche für d und a.

Lösung
PVector c = a;
PVector d = new PVector(11, -2);

println(c == a); // sind die Vektoren identisch?
println(c.equals(a)); // sind die Vektoren gleich?
println(d == a); // sind die Vektoren identisch?
println(d.equals(a)); // sind die Vektoren gleich?

Überlegen Sie vor Programmstart, was herauskommen sollte.

Übungsaufgaben

4.1 a) Addition/Subtraktion   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Erzeugen Sie die Vektoren v1 = (5, 5) und v2 = (3, 2) und berechnen Sie sowohl v1 + v2 als auch v1 - v2. Drucken Sie das Ergebnis jeweils auf die Konsole.

Die Aktion zur Subtraktion heißt sub().

4.1 b) Ball mit Vektor   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Lassen Sie einen Ball von links nach rechts über den Bildschirm fliegen. Verwenden Sie dazu einen Vektor. Der Ball soll in der Mitte des Bildschirms starten. Wenn der Ball rechts austritt, soll er links wieder eintreten.

4.1 c) Vektor als Hintergrund-Farbspeicher   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie eine einfache Ball-Animation (Ball von links nach rechts, dann wieder links eintretend).

Suchen Sie sich mit dem Farbselektor zwei Farben aus und notieren Sie die RGB-Werte. Ihr Programm soll jetzt den Hintergrund auf diese Farben setzen, je nachdem, ob Sie die Taste 1 oder 2 drücken. Verwenden Sie einen Vektor und "missbrauchen" Sie die x, y, z Werte, um die RGB-Werte für den Hintergrund zwischenzuspeichern.

Verwenden Sie void keyPressed() und key, um die Tasten abzufragen.

Wichtig: Sie benötigen genau einen Vektor.

4.1 d) Bälle mit Vektoren   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie ein Programm, wo ein Ball durch den 2D-Raum fliegt und von den Wänden abprallt. Verwenden Sie einen Vektor für die Position und einen Vektor für die Geschwindigkeit.

Wählen Sie als Startposition die Mitte des Bildschirms. Die Startgeschwindigkeit sollte im Bereich -3 bis 3 liegen, sowohl in x- als auch in y-Richtung.

4.1 e) Mauskollision   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Nutzen Sie die Methode dist(), um zu testen, ob der Ball aus den obigen Codeschnipseln mit der Maus kollidiert.

Tipp
Es hat etwas mit dem Radius des Balls zu tun. Sie können auch im Kap. 5 (If-Anweisung) nochmal nachlesen, wie das funktioniert.

Zusammenfassung

  • Eigenschaften und Methoden: Objekte sind komplexe Gebilde, die mehrere Informationen in Eigenschaften speichern können und Aktionen in Form von Methoden bereitstellen.
  • Klasse und Objekttyp: Der Typ oder die Klasse eines Objekts ist eine Art Bauplan, aus dem neue Objekte erzeugt werden können. Man kann Variablen eines solchen Typs anlegen, um ein Objekt darin zu speichern. Klassen sind gleichzeitig auch Datentypen. Im Gegensatz zu primitiven Datentypen nennen wir sie Objekttypen und schreiben diese immer groß (vergleiche primitiven Datentypen: int, float oder boolean).
  • Beispielklasse PVector: PVector ist eine Klasse zum Speichern von 2-dimensionalen (oder 3-dimensionalen) Vektoren oder Punkten.
  • Objekt erzeugen: Neue Vektor-Objekte werden mit nach dem Schema new PVector(5, 10); erzeugt, also das Schlüsselwort new, dann Klassenname und Klammern mit etwaigen Parametern.
  • Punktnotation: Auf die Eigenschaften eines Objekts kann man mit Punktnotation zugreifen, z.B. v2.x, wenn v2 ein Objekt des Typs PVector enthält. Ebenso ruft man Methoden auf, wie z.B. bei: v2.add(v1)

4.2 Strings

Sie kennen Strings aus Kapitel 2. Es handelt sich um einen Datentyp, der es erlaubt, Texte zu speichern. Texte spielen in enorm vielen Bereichen der Programmierung eine Rolle, z.B. wenn Daten in Dateien gespeichert, wenn Informationen aus dem Web gesammelt werden oder wenn mit Mensch oder KI interagiert werden soll.

Video: Methoden von Strings (14:05)

Klasse String und ihre Methoden

Neu ist jetzt für Sie, dass String auch eine Klasse ist, die (auch in Java) fest eingebaut ist. Ein konkreter String ist demnach ein Objekt. So können Sie ein neues String-Objekt erzeugen:

String name = new String("Harry");

Sie kennen bislang hauptsächlich die Kurzform, die Sie natürlich auch weiterhin verwenden sollten:

String name = "Harry";

Wie jede Klasse hat auch String eine Reihe von Methoden. Diese können Sie auf jedem String aufrufen. Die zwei wichtigsten Methoden sind

  • equals()
  • length()

Beachten Sie, dass length() mit Klammern geschrieben wird, weil es eine Methode ist, im Gegensatz zu der Eigenschaft "length" bei einem Array (ohne Klammern), denn bei einem Array handelt es sich intern um eine Instanzvariable des Array-Objekts.

Die Methode equals() ist deshalb so wichtig, weil Sie alle Vergleiche damit durchführen sollten und nicht mit ==, z.B.:

String a = "Harry";
String b = "Sally";

if (a.equals(b)) {
   println("samesame");
} else {
   println("different");
}

Zu beachten ist, dass ein mit Anführungszeichen definierter String - zum Beispiel "Harry" auch ein Objekt ist, auf dem Sie eine Methode aufrufen können. Das heißt Sie können schreiben:

String b = "Sally";

if ("Harry".equals(b)) {
   println("samesame");
} else {
   println("different");
}

Hier ein Überblick über interessante Methoden (siehe Processing-Referenz für eine Gesamtübersicht):

  • equals(): testet, ob der angegebene String inhaltsgleich mit dem String ist (gibt true oder false zurück)
  • length(): gibt die Länge des Strings zurück
  • contains(): testet, ob der angegebene String im eigenen String enthalten ist (gibt true oder false zurück)
  • indexOf(): einen String innerhalb eines anderen Strings suchen (gibt Position des gefundenen Strings oder -1 zurück)
  • toLowerCase(): gibt kleingeschriebene Variante zurück
  • toUpperCase(): gibt großgeschriebene Variante zurück
  • substring(): einen Teilstring herstellen - es wird der Startbuchstabe (inklusive) und der Endbuchstabe (exklusive) als Indexzahl angegeben (daran denken, dass der erste Buchstabe Index 0 hat!)

Wichtig ist, dass keine der Methoden den String, auf dem die Methode aufgerufen wird, verändert. Tatsächlich ist es so, dass Strings nie im Verlauf ihres Lebens verändert werden können. String-Objekte sind sogenannte Immutables. In der Praxis heißt das, dass Methoden wie toLowerCase() einen neuen String zurückgeben, der alte String aber erhalten bleibt.

String-Objekte können nie verändert werden (es sind sog. Immutables). Bei Methodenanwendung wird stattdessen ein neuer String erzeugt.

Warum sind String immutable? Zum einen hat das mit Sicherheit zu tun: Man möchte verhindern, dass bösartige Programmteile Strings manipulieren. Zum anderen hat es mit paralleler Verarbeitung zu tun (Multithreading), wo es zu Problemen kommt, wenn zwei parallel laufende Programmteile eine Datenstruktur verändern.

Zunächst mal ein Beispiel für eine Methode. Hier die Methode contains(), die prüft, ob ein gegebener String in dem String-Objekt vorkommt (Groß-/Kleinschreibung ist relevant):

String x = "Terminator";
println(x.contains("tor"));
println(x.contains("Tor"));
true
false

Beispiel für indexOf():

String x = "Terminator";
println(x.indexOf("er"));
1

Beispiel für toLowerCase() und toUpperCase():

String x = "Terminator";
println(x.toLowerCase());
println(x.toUpperCase());
terminator
TERMINATOR

Beispiel für substring():

String x = "I love processing.";
println(x.substring(2, 6));
love

Identität und Gleichheit (String-Pool)

Wir haben gesagt, dass man bei Strings immer die Methode equals verwenden sollte, um zu testen, ob zwei Strings gleich sind. Aus der Information, dass String immutable sind, könnte man schließen, dass es egal ist, ob man == oder equals verwendet. Dies stimmt aber nicht, weil Strings anders behandelt werden als "normale" Objekte.

Java legt Strings in einem speziellen Bereich ab, dem sogenannten String-Pool, der getrennt ist vom üblichen Objektspeicher.

String s = "hallo";

Im String-Pool werden aus Gründen der Ökonomie nur unterschiedliche Strings abgelegt. Dabei wird Groß-/Kleinschreibung berücksichtigt, d.h. "hallo" und "Hallo" sind unterschiedliche Strings. Wenn wir einen zweiten String erzeugen, der das gleiche enthält wie der erste String, wird also kein neues Objekt erzeugt. Stattdessen zeigt die neue Variable auf das bereits vorhandene String-Objekt.

So kommt es also, dass in diesem Fall == funktioniert, da beide Variablen auf das selbe Objekt zeigen.

String s = "hallo";
String s2 = "hallo";

println(s == s2);
true

Jetzt haben Sie auch die Möglichkeit, auf folgende Weise einen String zu erzeugen:

  String s3 = new String("hallo");

Diese Art der Stringerzeugung zwingt Java dazu, das String-Objekt im "normalen" Speicher abzulegen:

Entsprechend bekommen Sie unterschiedliche Ergebnisse beim Vergleich mit ==.

String s = "hallo";
String s2 = "hallo";
String s3 = new String("hallo");

println(s == s2);
println(s == s3);
true
false

Daher ist es umso schlimmer, bei Stringvergleichen == zu verwenden, weil es manchmal klappt und manchmal nicht.

Das heißt: bei Stringvergleichen immer equals verwenden! Es gibt tatsächlich so gut wie keine Situation, wo Sie die Identität von Strings testen wollen, es geht immer um den Inhalt eines Strings.

String s = "hallo";
String s2 = "hallo";
String s3 = new String("hallo");

println(s.equals(s2));
println(s.equals(s3));
true
true

String-Objekte werden in einem String-Pool verwalten, wo nur unterschiedliche String-Objekte abgelegt werden. Man sollte beim Vergleich von Strings immer die Methode equals verwenden.

String-Konkatenation

Wie Sie bereits in Kapitel 2 gelernt haben, wird das Pluszeichen im Zusammenhang mit Strings zur String-Konkatenation. Dies passiert immer dann, wenn mindestens ein Operand (links oder rechts) ein String ist. Beispiel:

String name = "Harry";
String message = "Hallo, " + name + "! Wie geht's?";
println(message);
Hallo, Harry! Wie geht's?

Sie können auch Zahlen und boolesche Werte mit Hilfe von String-Konkatenation zu einem String hinzufügen, sowohl als Variable oder als Wert (Literal):

int x = 15;
boolean wahrheit = true;
println("Zahl: " + x + ", Zahl: " + 10 + ", boolesch: " + wahrheit);
Zahl: 15, Zahl: 10, boolesch: true

Zahlen und boolesche Werte werden bei der String-Konkatenation automatisch in Strings umgewandelt.

Fingerübungen

a) Position

Erzeugen Sie eine Variable s mit dem String "hallo". Geben Sie die Position des ersten "l" (kleines L) auf der Konsole aus. Verwenden Sie dazu eine String-Methode.

Lösung
String s = "hallo";

println(s.indexOf("l"));

b) Großbuchstaben

Erzeugen Sie eine Variable s mit dem String "james bond". Erzeugen Sie eine zweite Variable s2, die den String von s in Großbuchstaben enthalten soll ("JAMES BOND"). Verwenden Sie dazu eine String-Methode.

Geben Sie zum Testen s2 auf der Konsole aus.

Lösung

Man beachte, dass toUpperCase einen neuen String zurückgibt und dass der Originalstring nicht verändert wird.

String s = "james bond";
String s2 = s.toUpperCase();
println(s2);

c) Teilstring

Sie haben den String:

String m = "Harry Potter";

Drucken Sie mit Hilfe von substring aus:

Pott
Lösung

Man muss hier zwei Dinge beachten: Erstens wird ab 0 gezählt, zweitens ist die zweite Angabe (hier die 10) exklusiv, d.h. dieses Zeichen wird nicht in den Substring mit aufgenommen.

String m = "Harry Potter";
println(m.substring(6, 10));

d) String-Check

Sie haben die Strings

String m = "Harry Potter";
String m1 = "Ha";
String m2 = "ha";

Testen Sie, ob die String m1 bzw. m2 in dem String m enthalten sind. Verwenden Sie eine geeignete String-Methode.

Lösung

Da die Methode contains Groß-/Kleinschreibung berücksichtigt, ist m1 enthalten und m2 nicht.

String m = "Harry Potter";
String m1 = "Ha";
String m2 = "ha";

println(m.contains(m1));
println(m.contains(m2));
true
false

e) String-Konkatenation

Sie haben die folgende Variable:

int euro = 5;

Geben Sie mit println unter Verwendung der Variable aus:

Ich habe 5 Euro.
Lösung
int euro = 5;
println("Ich habe " + euro + " Euro.");

Übungsaufgaben

4.2 a) Vergleich   Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Erzeugen Sie die Strings:

String s1 = "mama";
String s2 = "Mama";

Testen Sie mit equals(), ob die Strings für gleich befunden werden.

Erzeugen Sie einen neuen String s3, indem Sie s2 in die klein geschriebene Variante verwandeln (sehen Sie oben im Skript nach, wie die Aktion heißt).

Vergleichen Sie s1 und s3 mit equals. Sind diese gleich?

4.2 b) Vergleich ohne Groß-/Kleinschreibung   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Erzeugen Sie die Strings:

String m1 = "Harry Potter";
String m2 = "harry potter";

Vergleichen Sie die Strings mit equals. Sie bekommen ein false zurück, weil die Groß-/Kleinschreibung nicht identisch ist.

Wenden Sie jetzt toLowerCase so an, dass die Groß-/Kleinschreibung ignoriert wird. Schaffen Sie das, ohne neue Variablen zu erzeugen?

4.2 c) Familienname   Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Gegeben sei:

String n1 = "Thomas Mann";
String n2 = "Erika Mann";

Schreiben Sie eine If-Anweisung, die "gleiche Familie" ausgibt, wenn n1 und n2 den gleichen Nachnamen haben.

Versuchen Sie zunächst, die Position des Leerzeichens zu finden und dann entsprechend die Strings zu schneiden.

Testen Sie Ihr Programm auch mit

String n1 = "Thomas Mann";
String n2 = "Erika Schmidt";
String n1 = "Viktoria Mann";
String n2 = "Erika Mann";
String n1 = "Donald Duck";
String n2 = "Dagobert Duck";

4.2 d) Kleine Wanze   Level 41 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Schreiben Sie ein Programm, das das Wort "Wanze" darstellt. Bei jedem Mausdruck, wird das Wort um einen Buchstaben kürzer. Also erst "Wanze", dann "anze", dann "nze" und so weiter.

Sobald das Wort ganz verschwunden ist, erscheint wieder "Wanze" beim nächsten Mausdruck und das ganze beginnt von vorn.

4.2 e) Drei Wörter   Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart

Sie haben einen String mit zwei Leerzeichen und sollen das zweite und dritte Wort mit Hilfe von indexOf und substring herausholen und auf die Konsole schreiben.

String msg = "Harry Sally Larry";  

Nachdem Sie den Code geschrieben haben, verändern Sie den String.

String msg = "Marlene Cornelius Rudy";  

Ihr Code sollte immer noch funktionieren.

Zusammenfassung

  • Objekte: String ist kein primitiver Datentyp. Strings sind Objekte.
  • String-Methoden: Die Klasse String bietet wichtige Methoden an, um Strings zu analysieren (z.B. Länge und enthaltene Teilstrings) und neue Strings zu erzeugen (z.B. einen Teilstring "herausschneiden"). Wichtige Methdoden sind: equals, length, contains, indexOf, toLowerCase/toUpperCase, substring.
  • Vergleich mit equals: Strings sollten immer mit der Methode equals() verglichen werden, nicht mit dem == Operator.
  • Konkatenation: Mit dem Plus-Symbol kann man Strings aneinander reihen. Sie können Strings auch mit Zahlen und booleschen Werten kombinieren.

4.3 Bilder [optional]

PImage steht für "Processing Image" - diese Klasse erlaubt Ihnen, Bilder einzubinden. Unterstützt werden die Formate .gif, .jpg, .tga und .png. Die Bilder werden als Objekte gespeichert und funktionieren also ähnlich wie Objekte der Typen PVector und String.

Klassen, die mit einem "P für Processing" beginnen (und einige andere), stehen nicht in Java zur Verfügung. Nur als Vorwarnung.

Sie deklarieren zunächst eine Variable vom Typ PImage. Jetzt verwenden Sie nicht new, um ein neues Objekte herzustellen, sondern verwenden die Funktion loadImage(), um eine Datei zu laden und zu einem Objekt zu machen. Diese Datei muss in einem Unterverzeichnis mit Namen "data" liegen (im Verzeichnis Ihres aktuellen Programms). Nach Aufruf von loadImage() hat Processing aus den Bilddaten der Datei ein Bildobjekt hergestellt. Dies ist jetzt in der Variablen gespeichert.

Dann verwenden Sie das Kommando image(), um das Bild zu zeichnen. Als Paramter übergeben Sie das Bildobjekt und die Zielkoordinaten. Denken Sie daran, dass genauso wie rect() und ellipse() das Bild in jeder Runde von draw() neu gezeichnet wird.

PImage pic;

void setup() {
  pic = loadImage("face-smile.png");
}

void draw() {
  background(255);
  image(pic, width/2, height/2);
}

Die Bilder haben ebenfalls Eigenschaften. Interessant sind z.B. die Eigenschaften width und height, die Ihnen die Größe des Bildes zurückmelden. Nützlich, wenn Sie ein Bild fliegen lassen:

PImage devil;
int devilX = 0;
int xspeed = 1;

void setup() {
  devil = loadImage("face-devilish.png");
}

void draw() {
  background(0);
  image(devil, devilX, height/2);

  devilX += xspeed;
  if (devilX > width - devil.width || devilX < 0) {
    xspeed = -xspeed;
  }
}

Mit imageMode können Sie (ähnlich wie mit rectMode) beeinflussen, wo das Bild verankert ist, z.B. links oben (CORNER, dies ist der Default) oder in der Mitte (CENTER).

Dies ist nützlich, wenn Sie ein Bild mit der Maus hin- und herschieben wollen.

PImage player;
PImage devil;
int devilX = 0;
int xspeed = 2;

void setup() {
  size(200,200);
  imageMode(CENTER);
  player = loadImage("face-smile.png");
  devil = loadImage("face-devilish.png");
  devilX = devil.width/2;
}

void draw() {
  background(0);
  image(player, mouseX, mouseY);
  image(devil, devilX, 100);

  devilX += xspeed;
  if (devilX > width - devil.width/2 || devilX < devil.width/2) {
    xspeed = -xspeed;
  }
}

4.4 Bibliotheken: Audio [optional]

Ein wichtiger Grund, Objekte zu verwenden, ist es, Code an Dritte in Form sogenannter Bibliotheken weiterzugeben. Eine Bibliothek (engl. library) ist nichts anderes als eine Kollektion von Klassen, die jemand programmiert hat und zur Verfügung stellt.

Wenn man in Processing Sounds abspielen will, kann man z.B. die Bibliothek Minim verwenden. In Java nennt man (einen Teil einer) Bibliothek auch Paket.

Diese Bibliothek muss man zunächst aus dem Internet laden. Processing macht es einem sehr leicht. Es gibt einen Menüpunkt Sketch > Import Library..., unter dem man direkt minim auswählen kann. Anschließend wird die entsprechende Datei runtergeladen und ab sofort können Sie minim jederzeit verwenden.

Jetzt müssen Sie allerdings jedesmal im Code ankündigen, dass Sie ein externes Paket wie minim verwenden. Dies machen Sie in den obersten Zeilen Ihres Codes. Für minim schreiben Sie:

import ddf.minim.*;

Das bedeutet "Stelle mir alle Klassen aus dem Paket ddf.minim" zur Verfügung. Processing lädt dann alle diese Klassen in seinen Speicher.

In Minim gibt es zwei Objekte, die Sie brauchen:

  • Das Minim-Objekt kann man sich vorstellen wie eine Fabrik, die tragbare Audioplayer herstellt.
  • Den Audioplayer kann man sich vorstellen wie einen tragbaren MP3-Player, der genau einen Song abspielen kann. Man sagt der Fabrik, welchen Song man möchte, und die Fabrik stellt einen Player her, der diesen Song spielen kann Er hat einen Play-Button und einen Pause-Button. Wenn Sie einen anderen Song spielen wollen, müssen Sie sich einen weiteren Audioplayer aus der Fabrik holen.

Sie müssen sich erstmal die Fabrik erschaffen. Das funktioniert so:

Minim minim;
minim = new Minim(this);

Sie erstellen also ein Objekt - das sehen Sie an dem new. Das Objekt ist in der Variablen minim gespeichert. Es ist durchaus üblich, die Variable so zu nennen wie die Klasse, bloß klein geschrieben.

Jetzt können Sie sich aus der Fabrik den Audioplayer "rauslassen". Da der Audioplayer immer auf einen Song spezialisiert ist, müssen Sie sagen welcher Song auf Ihr Gerät kommen soll - dann bekommen Sie ein Audioplayer-Objekt zurück.

Minim minim;
minim = new Minim(this);
AudioPlayer player;
player = minim.loadFile("jump.mp3");

Wichtig: Ihre Sound-Datei - hier: jump.mp3 - muss im Verzeichnis Ihres Processing-Programms liegen, genauer in einem Unterverzeichnis mit Namen "data".

Jetzt können Sie diesen einen Sound, der auf dem Audioplayer liegt, mit der Methode play() abspielen. Wir bringen den Code mal in keyPressed() unter, damit wir das öfter testen können:

import ddf.minim.*;

Minim minim;
AudioPlayer player;

void setup() {
  minim = new Minim (this);
  player = minim.loadFile("jump.mp3");
}

// Kann man auch weglassen:
void draw() {
}

void keyPressed() {
  player.play();
}

(Für obigen Code benötigen Sie ein Audiofile namens jump.mp3 in dem data-Unterverzeichnis)

Wenn Ihr Sound länger ist, kann es sein, dass Ihr Play-Kommando nichts tut, weil der Sound noch läuft. Um das zu beheben, müssen Sie vor jedem play() den Sound auf die Nullposition zurücksetzen. Dazu verwenden Sie die Methode cue(). Diese Methode bekommt die gewünschte Position in Millisekunden. Wir nehmen einfach Null.

import ddf.minim.*;

Minim minim;
AudioPlayer player;

void setup() {
  minim = new Minim (this);
  player = minim.loadFile("jump.mp3");
}

// Kann man auch weglassen:
void draw() {
}

void keyPressed() {
  player.cue(0);
  player.play();
}

Wollen Sie gleichzeitig eine Hintergrundmusik laufen lassen, erzeugen Sie mit Ihrer Fabrik einfach einen zweiten Player:

import ddf.minim.*;

Minim minim;
AudioPlayer playerFX;
AudioPlayer playerBackground;

void setup() {
  minim = new Minim (this);
  playerFX = minim.loadFile("jump.mp3");
  playerBackground = minim.loadFile("music.mp3");
  playerBackground.loop();
}

// restlicher Code ...

(Für obigen Code benötigen Sie zwei Audiofiles namens jump.mp3 und music.mp3 in dem data-Unterverzeichnis)

Mit dem Befehl loop() läuft die Musik im Wiederholmodus. Sie können die Hintergrundmusik mit pause() und play() steuern, z.B. per Tastendruck. Wir hängen den Jump-Sound an die Leertaste und das Play/Pause an die Taste P. Um zwischen Play und Pause umzuschalten, nutzen wir die Methode isPlaying(), die zurückgibt, ob der Player im Play- oder Pause-Modus ist (boolean).

void keyPressed() {
  if (key == ' ') {
    playerFX.cue(0);
    playerFX.play();
  }

  if (key == 'p') {
    if (playerBackground.isPlaying()) {
      playerBackground.pause();
    } else {
      playerBackground.play();
    }
  }
}

Hier finden Sie mehr Dokumentation zu den Klassen Minim und AudioPlayer.

Bitte achten Sie darauf, welche Lizenz ein Sound hat. Sie müssen zunächst darauf achten, dass der Sound kostenlos ist, aber darüber hinaus, ob eine kommerzielle Nutzung zulässig ist (falls Sie Ihr Spiel verkaufen möchten). Ebenfalls wichtig ist, ob die Nennung des Erschaffers notwendig ist (z.B. bei der Creative Commons Attribution 3.0). Wenn ja, üben Sie gleich das "Credit geben", indem Sie in Ihrem Code-Kommentar die Sound-Erschaffer angeben. Lesen Sie sich unbedingt die Lizenzbedingungen auf der jeweiligen Download-Webseite durch.