Und nochmal zum Einkaufszettel: werde interaktiver!
Erinnerst du dich an unseren Einkaufszettel?
So:
public class Einkaufszettel {
private ArrayList<String> einkauf = new ArrayList<>();
public void einkaufHinzufügen(String artikel) {
this.einkauf.add(artikel);
}
public void druckeListe() {
for (String art : this.einkauf) {
println(art);
}
}
public void druckeNummerierteListe() {
for (int i=0; i<this.einkauf.size(); i++) {
println(i + ": " + this.einkauf.get(i));
}
}
public void tauscheArtikel(int indexA, int indexB) {
String zwischenspeicherArtikelA = this.einkauf.get(indexA);
this.einkauf.set(indexA, this.einkauf.get(indexB));
this.einkauf.set(indexB, zwischenspeicherArtikelA);
}
}
Kannst du ein paar Fragen dazu beantworten?
Sehr schön! Hier ist die erste Frage:
In welcher der hier genannten Methoden wird die ArrayList einkauf
nicht traversiert?
Das ist leider falsch – in druckeListe
wird eine erweiterte for
-Wiederholung zur Traversierung genutzt.
Das ist leider falsch – in druckeListe
wird eine for
-Wiederholung zur Traversierung genutzt.
Das ist richtig, gut gemacht!
Welcher Fehler wird geworfen, wenn die Methode tauscheArtikel
mit Indizes aufgerufen wird, die nicht in der ArrayList einkauf
enthalten sind?
Das stimmt leider nicht: ein solcher Fehler würde geworfen, wenn du ein Objekt aufrufen würdest, was gar nicht da ist. Das wäre so ähnlich, wie wenn du zu einer Hausnummer gehen würdest, und dann wäre das Haus nicht da.
Hier ist es anders: Wenn du versuchst, einen Index aufzurufen, der nicht in der Liste ist, wäre das so, wie wenn du eine Hausnummer hättest, die es gar nicht gibt, weil die Straße nicht so lang ist. Richtig wäre also: Index out of bounds
Das ist richtig, sehr gut!
Das stimmt leider nicht. Ein solcher Fehler würde geworfen, wenn du statt einer Zahl als Index eine Zeichenkette übergeben würdest.
Hier ist es anders: Wenn du versuchst, einen Index aufzurufen, der nicht in der Liste ist, wäre das so, wie wenn du eine Hausnummer hättest, die es gar nicht gibt, weil die Straße nicht so lang ist. Richtig wäre also: Index out of bounds
Wie viele Instanzvariablen hat die Klasse Einkaufszettel
?
Das stimmt leider nicht (selber haha…). Hier wird eine Referenz auf eine ArrayList
gehalten, und zwar in der Instanzvariable einkauf
. Auch wenn die ArrayList
selbst wieder beliebig viele Zeichenketten abspeichern kann, ist sie selbst nur ein Objekt – und das wird über die Instanzvariable einkauf
erreicht.
Das ist richtig! Hier wird eine Referenz auf eine ArrayList
gehalten, und zwar in der Instanzvariable einkauf
. Auch wenn die ArrayList
selbst wieder beliebig viele Zeichenketten abspeichern kann, ist sie selbst nur ein Objekt – und das wird über die Instanzvariable einkauf
erreicht.
Sehr schön, das ist richtig! einkauf
ist die gesuchte Instanzvariable.
Das ist so leider nicht richtig. Hier wird eine Referenz auf eine ArrayList
gehalten, und zwar in der Instanzvariable einkauf
. Auch wenn die ArrayList
selbst wieder beliebig viele Zeichenketten abspeichern kann, ist sie selbst nur ein Objekt – und das wird über die einzige Instanzvariable einkauf
erreicht.
Dann kann es losgehen! Heute wirst du eine Struktur kennenlernen, die in vielen Programmen dafür sorgt, dass wir als Benutzer_in damit umgehen können.
Eine potentiell unendliche Wiederholung. Manche Programmierer_innen sprechen auch von einem Mainloop. Die Wiederholung wird immer wieder und wieder durchlaufen, bis unsere Nutzer_in irgendwann entscheidet, das Programm zu beenden.
Eine gute Frage… vielleicht kannst du dir einen Butler oder Sekretär vorstellen, der dich fragt, was auf die Liste soll? Der könnte dich solange fragen, ob du noch etwas möchtest, bis du z.B. »Danke« eintippst.
Vielleicht so:
Sekretär johann = new Sekretär();
while(johann.istEmpfangsbereit()) {
johann.frageNachAuftrag();
}
Diese vier Zeilen sind dann der Einstiegspunkt zu unserem Programm. Schreibe sie in eine Datei »Start« in der Online-IDE im selben Workspace wie die Klasse Einkaufszettel
.
Das kann ich mir vorstellen! Du musst ja noch die Klasse Sekretär
anlegen, und weder die Methode istEmpfangsbereit
noch die Methode frageNachAuftrag
gibt es schon…
An dieser Stelle könntest du versuchen, alleine weiterzumachen. Unser Sekretär soll vier Anweisungen verstehen:
schreibe
zeige
tausche
danke
Die erste Anweisung sieht z.B. so aus. Du tippst als Nutzer_in »schreibe Müsli«, und unser Sekretär nimmt das Wort »Müsli« in seinen Einkaufszettel auf. Als nächstes würdest du vielleicht »schreibe Joghurt« eintippen, und auch das Wort »Joghurt« würde auf der Liste landen.
Wenn du »zeige« eintippst, soll dir der Sekretär die bisher aufgenommene Einkaufslist zeigen. In unserem Fall würde das dann so aussehen:
0: Müsli
1: Joghurt
Ja, durchaus – dann können wir mit »tausche 0 1« nämlich einen Tausch der Reihenfolge vornehmen. Ein erneutes »zeige« würde dann zeigen, dass der Befehl erfolgreich war:
0: Joghurt
1: Müsli
»danke« würde das Programm beenden – irgendwie müssen wir ja aus der Unterhaltung auch wieder aussteigen können. Ein vollständiger Durchlauf könnte z.B. so aussehen:
Was kann ich für Sie tun?
schreibe Müsli
Was kann ich für Sie tun?
schreibe Joghurt
Was kann ich für Sie tun?
zeige
0: Müsli
1: Joghurt
Was kann ich für Sie tun?
tausche 0 1
Was kann ich für Sie tun?
zeige
0: Joghurt
1: Müsli
Was kann ich für Sie tun?
danke
Auf Wiedersehen!
Wenn du ab hier alleine weitermachen möchtest, nur zu! Wenn du diese Aufgabe alleine lösen kannst, wirst du sicher eine Menge dabei lernen. Falls du Hilfe brauchst, kannst du sie hier bekommen.
Das geht klar. Dein erster Schritt sollte sein, das Programm zum Laufen zu bekommen (und als zweiten Schritt: zum Fliegen…)
Orientiere dich an den Fehlermeldungen, die das Start-Skript noch ausgibt. Schreibe als erstes eine Klasse Sekretär
in eine neue Datei, im gleichen Workspace in der Online-IDE.
class Sekretär {
}
Gut!
Dann werden die beiden Methoden istEmpfangsbereit
und frageNachAuftrag
noch nicht gefunden. Schreibe zunächst die Methode istEmpfangsbereit
– sie soll zunächst nur immer den Wahrheitswert true
zurückgeben. (Um die Details kümmern wir uns später.)
class Sekretär {
public boolean istEmpfangsbereit() {
return true;
}
}
Sehr schön!
Nun fehlt noch die Methode frageNachAuftrag
. Sie hat keinen Rückgabetyp (wir brauchen also void
), und sie soll zunächst nur ausgeben: »Was kann ich für Sie tun?«.
Wie die erste in die Klasse Sekretär
, nach der schließenden Klammer }
der Methode istEmpfangsbereit
, aber vor der letzten schließenden Klammer }
der Klasse:
public void frageNachAuftrag() {
println("Was kann ich für Sie tun?");
}
Gut!
Jetzt sollte das Programm bereits laufen. Probiere es einmal aus!
Das hört sich gut an – so soll es sein. Klicke auf das rote Quadrat, um die Ausführung abzubrechen.
Falls du immer noch eine Fehlermeldung bekommst, versuche diese zu verstehen und den Fehler zu beheben. Ansonsten vollziehe noch einmal alle Schritte bis hierhin nach, und sieh’ zu, dass du das Programm zum Laufen bekommst. Es sollte bis zu diesem Punkt lediglich die Ausgabe »Was kann ich für Sie tun?« wieder und wieder ausgegeben werden.
Jetzt bist du soweit, die eigentliche Funktionialität zu programmieren. Gut ist, dass das Programm bereits läuft – so kannst du während des Programmierens jederzeit ausprobieren, ob das Programm das tut, was du möchtest.
Am besten heilen wir erst einmal einen objektorientierten Fauxpas, der gerade noch in unserer Klasse enthalten ist. So tut unser Sekretär-Objekt zwar, als hätte es eine Zustand, hat aber gar keinen.
Naja, es behauptet jedes Mal, wenn es gefragt wird, empfangsbereit zu sein. Im Augenblick wird es niemals behaupten, nicht empfangsbereit zu sein, und false
zurückgeben.
Wir verpassen ihm eine Instanzvariable, und geben diese in der Methode zurück. Auf diese Weise können wir später, wenn wir es brauchen, den Zustand unseres Sekretär-Objektes einfach ändern.
Alles klar!
Instanzvariablen schreiben wir gleich nach der Zeile mit der Klassendefinition auf:
class Sekretär {
boolean istEmpfangsbereit = true;
// weitere Code ist hier nicht dargestellt
}
Nun fehlt noch, dass unsere gleichnamige Methode (sie könnte übrigens auch anders heißen) den in der Variable abgespeicherten Wert auch zurückgibt.
Sehr schön!
Statt true
geben wir einfach die Variable zurück:
public boolean istEmpfangsbereit() {
return istEmpfangsbereit;
}
Wenn du es ganz wunderschön schreiben möchtest, könntest du auch this.istEmpfangsbereit
zurückgeben – das macht hier aber keinen Unterschied.
Wenn du das Programm jetzt laufen lässt, sollte es sich genau wie vorher verhalten. Probier es einmal aus!
Unsere Veränderung war sinnvoll. Ohne die Variable hätte sich das von uns verwendete Sekretär-Objekt nicht daran erinneren können, ob es empfangsbereit ist, oder vielleicht nicht mehr. Diese Information wird aber später wichtig werden, wenn unser Programm ordnungsgemäß beendet werden können soll.
Vielleicht benutzen wir unsere Variable auch gleich? Wenn »Danke« eingegeben wird, soll das Programm ja beendet werden. Das ist der Fall, wenn unser Sekretär false
zurückgibt.
Die Zeile
String eingabe = Input.readString("Was kann ich für Sie tun?");
gibt unseren Prompt aus und liest eine Nutzereingabe in der Online-IDE ein. Jetzt könntest du versuchen, den Rest der Funktionialität zu implementieren. Bekommst du es hin, dass das Programm nach Eingabe von »Danke« beendet wird?
Sehr schön!
Zunächst solltest du die Zeile println("Was kann ich für Sie tun?");
durch die oben angegebene Zeile ersetzen, so dass die Methode so aussieht:
public void frageNachAuftrag() {
String eingabe = Input.readString("Was kann ich für Sie tun?");
}
Kannst du diese Anweisung nach Java übersetzen?
Wenn die Eingabe gleich ist wie das Wort »Danke«, setze die Variable eingabe
auf false
.
Wenn du es hinbekommst, wärst du eigentlich schon fertig…
Yeah!
public void frageNachAuftrag() {
String eingabe = Input.readString("Was kann ich für Sie tun?");
if (eingabe == "Danke") {
istEmpfangsbereit = false;
}
}
Eine Sache ließe sich noch etwas verbessern…
Stringvergleiche sind potentiell unsicher, oder können zu schwer nachzuvollziehenden Fehlern führen, wenn du den ==
Operator verwendest… Daher gibt es eine equals
-Methode, die String-Objekte für Vergleiche verwenden können. Daneben gibt es noch eine schöne Variante, die die Groß- und Kleinschreibung vernachlässigt. Diese equalsIgnoreCase
genannte Methode sorgt dafür, dass wir uns darum keine Gedanken mehr machen müssen. Wir müssen dazu nur diese Kleinigkeit ändern:
public void frageNachAuftrag() {
String eingabe = Input.readString("Was kann ich für Sie tun?");
if (eingabe.equalsIgnoreCase("Danke")) {
istEmpfangsbereit = false;
}
}
Dann auf zum nächsten Befehl! Der Befehl »schreibe« sollte der nächste sein – ohne irgendetwas auf der Liste ist weder »zeige« noch »tausche« sinnvoll…
Nun stehen wir vor eine kleinen Schwierigkeit – neben dem Befehl sollen ja auch noch Dinge eingegeben werden, die unser Sekretär auf die Liste schreiben soll. Siehst du, was daran problematisch sein könnte?
Das stimmt! Wenn wir diese Funktionalität genau so wie beim »Danke«-Befehl programmieren würden, hätten wir Probleme. Wir können ja auch nicht voraussehen, welche Artikel auf die Einkaufsliste kommen sollen…
Genau das müssten wir machen! Programmierer_innen sprechen übrigens vom Parsen. Das kann ganz schön kompliziert werden… hier nehmen wir allerdings keine Rücksicht auf Grenzfälle, nehmen Abstürze in Kauf und schreiben nur Ein-Wort-Artikel auf unsere Einkaufsliste – das macht unser Programm etwas kürzer und übersichtlicher.
Eine einfache Methode, die Eingabe aufzuteilen, sieht so aus:
String[] eingabeGeparst = eingabe.split(" ");
Erinnerst du dich an das Kapitel über Arrays? Falls du das nicht bearbeitet hast: wir bekommen von der split
-Methode ein Array von Zeichenketten zurück. Ein Array ist so etwas wie eine ArrayList, nur dass es etwas anders angesprochen wird. Für uns wichtig zu wissen ist, dass wir mit eingabeGeparst[0]
das erste Element bekommen, mit eingabeGeparst[1]
das zweite, und so weiter… Im Array enthalten sind alle einzelnen Wörter der Eingabe, die durch Leerzeichen getrennt waren. (Dafür haben wir der split
-Methode den String " "
übergeben.)
Auf den Einkaufszettel… oh nein, den gibt es ja noch gar nicht! Unser Sekretär kennt den Einkaufszettel ja noch gar nicht… Das sollten wir noch schnell ändern, und er bekommt ein Einkaufszettelobjekt als Instanzvariable:
class Sekretär {
boolean istEmpfangsbereit = true;
Einkaufszettel zettel = new Einkaufszettel();
// weitere Code ist hier nicht dargestellt
}
Jetzt könntest du bereits die Funktionalität für den »schreibe«-Befehl implementieren. Bekommst du das hin?
Sehr schön!
Unser Code entwickelt zwar langsam Spaghetti-Code-Qualitäten, aber das bekommen wir vielleicht später noch in den Griff… wir schreiben einfach weiter:
public void frageNachAuftrag() {
String eingabe = Input.readString("Was kann ich für Sie tun?");
if (eingabe.equalsIgnoreCase("Danke")) {
istEmpfangsbereit = false;
}
String[] eingabeGeparst = eingabe.split(" ");
if (eingabeGeparst[0].equalsIgnoreCase("schreibe")) {
zettel.einkaufHinzufügen(eingabeGeparst[1]);
}
}
Jetzt sollte also der Befehl »schreibe« funktionieren.
Da hilft die Online-IDE weiter. Während das Programm läuft, kannst du die Objekte, die wir erzeugt haben, untersuchen. Verwende den Befehl, und lass dir dann anzeigen, wie die Objekte genau aussehen:
Nein, natürlich nicht! »2 El« steht hier für zwei Elemente…
»Zeige« erscheint mir sinnvoll… Weißt du, was zu tun ist?
Sehr schön!
public void frageNachAuftrag() {
// Spaghetti-Code vom Anfang der Methode…
// eingabe ist lokal in der Methode bekannt
// zettel ist eine Instanzvariable
if (eingabe.equalsIgnoreCase("zeige")) {
zettel.druckeNummerierteListe();
}
}
Dann fehlt nur noch ein Befehl! Bekommst du »tausche« alleine hin?
Super!
Der ist auch etwas kompliziert… du hast sicher schon herausbekommen, dass in eingabeGeparst[1]
der eine Index steht, und in eingabeGeparst[2]
der andere, doch handelt es sich dabei nicht um int
-Zahlen…
Das ist ein ganz übliches Problem! Du könntest es sogar herausbekommen, wenn du nach »java typecast String int« im Internet suchst. Du wirst sicher schnell eine Stackoverflow-Seite finden, auf der eine Lösung steht.
Es gibt mehrere Möglichkeiten. Eine ist, eine so genannte statische Methode der Klasse Integer
aufzurufen, die das kann: sie nimmt einen String entgegen, und gibt eine ganze Zahl zurück. (Oder: wirft einen Fehler, wenn sich der String nicht umwandeln lässt.)
Sie heißt parseInt
und lässt sich so verwenden:
int indexA = Integer.parseInt(eingabeGeparst[1]);
Ähem… versuchst du es selber? Ich würde es so schreiben:
public void frageNachAuftrag() {
// Spaghetti-Code vom Anfang der Methode…
// eingabeGeparst ist das lokal Array
// zettel ist eine Instanzvariable
if(eingabeGeparst[0].equalsIgnoreCase("tausche")) {
int indexA = Integer.parseInt(eingabeGeparst[1]);
int indexB = Integer.parseInt(eingabeGeparst[2]);
zettel.tauscheArtikel(indexA, indexB);
}
}
Dann sollte jetzt alles funktionieren! Probiere das Programm mit verschiedenen Eingaben aus!
Sehr schön!
Hast du eigentlich schon einmal etwas vom switch
-Statement gehört?
Das stimmt – gut gesehen!
Nun ja, vielleicht sieht deine Methode frageNachAuftrag
ja so aus:
public void frageNachAuftrag() {
String eingabe = Input.readString("Was kann ich für Sie tun?");
if(eingabe.equalsIgnoreCase("Danke")) {
istEmpfangsbereit = false;
}
String[] eingabeGeparst = eingabe.split(" ");
if(eingabeGeparst[0].equalsIgnoreCase("schreibe")) {
zettel.einkaufHinzufügen(eingabeGeparst[1]);
}
if(eingabe.equalsIgnoreCase("zeige")) {
zettel.druckeNummerierteListe();
}
if(eingabeGeparst[0].equalsIgnoreCase("tausche")) {
int indexA = Integer.parseInt(eingabeGeparst[1]);
int indexB = Integer.parseInt(eingabeGeparst[2]);
zettel.tauscheArtikel(indexA, indexB);
}
}
Das ist richtig! Mit etwas besserer Organisation ließen sich die einzeln stehenden if
-Blöcke mit else if
-Zeilen miteinander verbinden. Aber anstattdessen ließe sich auch sehr elegant und übersichtlich ein switch
-Statement verwenden.
Wenn eine Variable auf mehrere Fälle hin überprüft werden soll, lässt sich so einiges an Schreibarbeit sparen – das macht gleichzeitig den Code übersichtlicher. switch
-Statements sehen dann so aus:
switch(meineVariable) {
case "fallEins":
// Code, der Fall eins behandelt
break;
case "fallZwei":
// Code, der Fall eins behandelt
break;
// viele weitere Fälle möglich
default:
// Hier kommt Code hin, der ausgeführt wird,
// wenn keiner der Fälle zutrifft
}
break
verlässt den switch
-Block. Wenn es fehlt, würde der Code, der in den folgenden Fällen folgt, auch noch ausgeführt. (Das lässt sich manchmal geschickt ausnutzen.) Üblicherweise endet aber jede Fallunterscheidung mit case
mit einer break
-Anweisung.
Versuche doch einmal selbst, die bisherige Implementierung mit switch
-Statement umzuschreiben!
Sehr schön!
Nun, zum Beispiel ließe es sich so schreiben:
public void frageNachAuftrag() {
String eingabe = Input.readString("Was kann ich für Sie tun?");
String[] geparsteEingabe = eingabe.split(" ");
switch(geparsteEingabe[0].toLowerCase()) {
case "schreibe":
zettel.einkaufHinzufügen(geparsteEingabe[1]);
break;
case "zeige":
zettel.druckeNummerierteListe();
break;
case "tausche":
int indexA = Integer.parseInt(geparsteEingabe[1]);
int indexB = Integer.parseInt(geparsteEingabe[2]);
zettel.tauscheArtikel(indexA, indexB);
break;
case "danke":
println("Auf Wiedersehen!");
this.istEmpfangsbereit = false;
break;
default:
println("Das habe ich nicht verstanden.");
println("Ich verstehe »schreibe«, »zeige« und »tausche«.");
}
}
Das ist richtig. Hier wird zum Beispiel nur die geparste Eingabe angesehen. Werden die Eingaben wie vorgesehen gemacht, steht aber immer an erster Stelle der Befehl, der uns interessiert. Daneben sind noch einige nette Ausgaben hinzugefügt.
Versuche es doch einmal! Ein break
wegzulassen wäre sicher ein interessantes Feature…
Dann sind wir fertig, und du weißt jetzt, wie du einem Programm eine gute Struktur verpasst. Vielleicht hast du sogar ein switch
-Statement zum ersten Mal eingesetzt!
Neue Vokabeln in dieser Lektion
Schreibe den entsprechenden Code auf, und überprüfe, ob du richtig liegst!
Einen Mainloop für ein Objekt spiel
schreiben, das so lange wiederholt wird, so lange die Methode istLaufend
true
zurückgibt.
while(spiel.istLaufend()) {
// Code, der wiederholt durchlaufen wird
}
Ein switch
-Statment, das die String-Variable römisch
auf die Fälle I, V und X überprüft und der int-Variable wert
die entsprechende Zahl zuweist (als römische Ziffer interpretiert). Per default soll wert
auf 0 gesetzt werden.
switch(römisch) {
case "I":
wert = 1;
break;
case "V":
wert = 5;
break;
case "X":
wert = 10;
break;
default:
wert = 0;
}