- 04.11.2024: Korrektur in 4.1 (Rechnung in Diagramm), Betrag hinzugefügtˇ
- 03.10.2024: Challenge hinzugefügt
- 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
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.
Challenge
Was wollen Sie nach dem Durcharbeiten des Kapitels programmieren können?
In dem Fenster sehen Sie einen fliegenden Ball und seinen Geschwindigkeitsvektor. Sie können den Vektor mit den Pfeiltasten verändern.
Schaffen Sie es, dieses Programm zu schreiben?
Sie finden die dazugehörige Aufgabe unter 4.1 (5).
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 Was ist ein Vektor?
Ein Vektor im zweidimensionalen Raum ist einfach ein Pfeil in einem zweidimensionalen Koordinatensystem. Dabei ist es "egal", wo genau dieser Pfeil platziert ist. Wichtig ist nur, in welche Richtung er zeigt und wie lang er ist. Üblicherweise beschreibt man einen 2D-Vektor mit genau zwei Zahlen: die erste Zahl beschreibt den x-Anteil, die zweite Zahl den y-Anteil. (Man spricht auch von x- oder y-Komponente.) Die Länge kann man dann berechnen (s. unten).
Hier sehen wir ein Beispiel eines Vektors v mit x-Anteil 3 und y-Anteil 2:
Wenn man sich einen animierten Ball vorstellt, kann man sich einen Vektor als die Bewegungsrichtung vorstellen. Die Länge des Vektors beschreibt dabei die Geschwindigkeit des Balls. Es ist wichtig, das ein Vektor wie v nicht an einen bestimmten Ort im Koordinatensystem gebunden ist. Die Tatsache, dass der Vektor in der obigen Abbildung bei dem Punkt (2, 1) eingezeichnet ist, spielt keine Rolle.
Addition von zwei Vektoren
Zwei Vektoren kann man addieren und das hat eine sehr intuitive räumliche Bedeutung, wie man hier sieht:
Die Vektoren v und w werden addiert, indem man die jeweiligen x-Komponenten und y-Komponenten addiert. Räumlich ergibt sich genau der Vektor, den man bekommt, wenn man die beiden Vektoren hintereinander setzt und die Verbindung vom Startpunkt von v zum Endpunkt von w zieht.
Länge eines Vektors
Wie lang ein Vektor ist, kann man mit dem Satz des Pythagoras ausrechnen. Man spricht auch vom Betrag eines Vektors. Für Vektor v schreibt man den Betrag so: |v|. Im Englischen nennt man den Betrag auch magnitude. Die Abbildung zeigt, wie man die Länge ausrechnet. Der x-Anteil und der y-Anteil sind die Seiten eines rechtwinkligen Dreiecks. Die Hypothenuse ist die gesuchte Länge. Damit sollte die Rechnung klar sein:Was ist ein Punkt?
Ein Punkt ist eine Position im Koordinatensystem, die mit x-Koordinate und y-Koordinate beschrieben wird. Obwohl ein Punkt streng genommen kein Vektor ist, beschreibt man einen Punkt mit dem gleichen Mechanismus. Ein Punkt wird so behandelt wie ein Vektor, der am Nullpunkt startet und zu dem entsprechenden Punkt führt. Punkte und Vektoren kann man daher einfach addieren, wie hier den Punkt p1 und den Vektor v:
Tatsächlich funktioniert so Animation im Computer. Man definiert eine Bewegung über einen Vektor wie oben v. Dann verändert man alle Punkte, die man bewegen möchte, indem man Vektor v auf diese addiert. Im Beispiel oben erhält man so die neue Position p2. Versuchen Sie gedanklich immer, klar zu unterscheiden, ob es um einen Punkt oder um einen Vektor geht.
4.2 Vektor als Beispiel-Objekt
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! Wir nennen den Vektor velocity
, weil dieser Begriff im Englischen andeutet, dass auch die Richtung mit enthalten ist.
PVector pos = new PVector(); // Nullvektor PVector velocity = new PVector(3,2); void draw() { background(255); ellipse(pos.x, pos.y, 20, 20); pos.add(velocity); 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).
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.
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.
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.2 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.2 b) Einfacher 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.2 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.2 d) Ball 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 1 bis 4 liegen, sowohl in x- als auch in y-Richtung.
Mit der Leertaste kann der User anstoßen, dass ein neuer Geschwindigkeitsvektor gewählt wird und der Ball ab der Mitte startet.
4.2 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.
4.2 f) Vektor-Labor Level 41 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie wieder ein Programm, wo ein Ball durch den Raum fliegt und von den Wänden abprallt. Verwenden Sie einen Vektor für die Position und Geschwindigkeit.
Jetzt sollen Sie den Vektor durch einen Strich darstellen. Damit man diesen sehen kann, multiplizieren Sie die jeweiligen x/y-Komponenten des Geschwindigkeitsvektors mit 10.
Der User kann jetzt den Geschwindigkeitsvektor mit den Pfeiltasten verändern. Mit rechts/links wird die x-Geschwindigkeit um 1 erhöht/gesenkt. Mit hoch/runter wird die y-Geschwindigkeit entsprechend.
Mit der Leertaste kann der User anstoßen, dass ein neuer Geschwindigkeitsvektor gewählt wird und der Ball ab der Mitte startet.
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.3 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.
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.
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
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.
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.
int euro = 5; println("Ich habe " + euro + " Euro.");
Übungsaufgaben
4.3 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.3 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.3 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.3 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.3 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.4 Bilder
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.5 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.