Folge 21.2: Ein Codebuffer

Schritt 3 - Steuercode

Auf Dauer ist es recht lästig, wenn man den Quellcode der Klasse Test für jede neue Berechnung neu schreiben muss. Ideal wäre es, wenn der Steuercode für die Stackmaschine in einer Textdatei stünde, die vielleicht folgendermaßen aufgebaut wäre:
push 4 
push 3.14 
add 
push 8 
push 2.71 
sub 
mul

Dies ist wieder das Beispiel zur Berechnung des Ausdrucks

(4 + 3.14) * (8 - 2.71)

Was diese sieben Zeilen der Textdatei genau bewirken, müsste Ihnen jetzt eigentlich klar sein. Es handelt sich um Befehle für die Stackmaschine. Ein solcher Befehl hat stets das Format

code arg

Ein Stackmaschinen-Befehl besteht also aus zwei Komponenten: Dem eigentlichen Befehl, z.B. push, add oder pop, und eventuell einem Argument. Der push-Befehl benötigt z.B. die zu pushende Zahl als Argument, während der add- oder der pop-Befehl kein Argument benötigen.

Übung 21.3 (3 Punkte)

Schreiben Sie den Stackmaschinen-Code zur Berechnung folgenden Ausdrucks auf:

((24 - 17) * (38 + 12) - 8) * (5 + 2)


Schritt 4 - Textdateien lesen

Nun kommt mal wieder etwas völlig Neues - sowohl theoretisch wie aus programmierpraktischer Sicht. Wie kann ein Java-Programm eine Textdatei Zeile für Zeile einlesen und dann auswerten? Wir wollen uns zunächst mit dem ersten Teil dieser Frage beschäftigen: Wie kann eine Textdatei Zeile für Zeile eingelesen und angezeigt werden?

Lesen Sie sich dazu bitte den Lexikon-Eintrag "Textdateien einlesen" durch und kommen Sie anschließend wieder hierhin zurück.

Übung 21.4 (4 Punkte)

Übertragen Sie das Gelernte in eine neue Klasse Codebuffer. Diese Klasse soll neben dem Konstruktor folgende Methoden haben:

public void liesDatei(String dateiname) throws IOException

Diese Methode kann ähnlich aufgebaut sein wie die Methode laden(), die Sie im Lexikon-Eintrag "Textdateien einlesen" kennengelernt haben. Allerdings sollen die Zeilen nicht in die Konsole ausgegeben werden wie in laden(), sondern in einem String-Array zwischengespeichert werden, damit andere Methoden später auf jede einzelne Zeile zugreifen können. Sie müssen die Klasse Codebuffer also mit einem String-Array ausstatten.

Übung 21.5 (2 Punkte)

Die in den String-Array eingelesenen Zeilen sollen jetzt durch eine Methode

public void anzeigen()

in der Konsole angezeigt werden. Ein Testprogramm für die Klasse Codebuffer müssen Sie noch nicht schreiben, es reicht, wenn Sie durch Aufruf der Methoden liesDatei() und anzeigen() demonstrieren, dass liesDatei() funktioniert. Vergessen Sie nicht, eine entsprechende Textdatei mit Stackmaschinencode in ihrem Projektordner zu platzieren.


Schritt 5 - Steuercode ausführen

Nachdem Sie jetzt eine Klasse Codebuffer geschrieben haben, die in der Lage ist, Steuercode für eine Stackmaschine zu lesen und zu speichern, wollen wir uns jetzt um den nächsten größeren Schritt in unserem Stackinterpreter-Projekt kümmern. Der Steuercode soll interpretiert werden.

Wenn also ein Befehl wie

Push 17

erkannt wird, dann soll die Zahl 17 auf den Stack der Stackmaschine gepusht werden. Die Frage ist nur, wer ist eigentlich für die Zusammenarbeit von Stackmaschine und Codebuffer verantwortlich? Vom Projekt-Design her sind mehrere Lösungen denkbar. Wenn Sie interessiert an dieser Frage sind und/oder eine Klausur in Informatik schreiben wollen/müssen, lesen Sie bitte den folgenden Theorieteil.

Exkurs für Abiturienten und Klausurleute:
Drei Konstruktionsvorschläge für das Stackinterpreter-Projekt.

Fassen wir jetzt also einmal zusammen (nähere Einzelheiten siehe Exkurs):

Wir brauchen eine Klasse Stackinterpreter, die dann Attribute der Klassen Stackmaschine, Codebuffer und - später - Variablenliste hat.

Der Stackinterpreter gibt den Befehl zum Einlesen des Stackcodes an das Codebuffer-Objekt weiter. Das Codebuffer-Objekt speichert die Zeilen der Textdatei in einem String-Array (die einfachste Lösung; Sie können das Ganze natürlich auch anderes implementieren, zum Beispiel mit einer dynamischen Liste). Dann holt sich der Interpreter den ersten Befehl aus dem Codebuffer und interpretiert ihn. Handelt es sich zum Beispiel um den Befehl

Push 3.14

so ruft der Interpreter das Stackmaschinen-Objekt auf und weist es an, die Zahl 3.14 auf den Stack zu pushen. Das hört sich alles ziemlich kompliziert an, nicht wahr.

Übung 21.6 (3 Punkte)

Erstellen Sie jetzt die Klasse Stackinterpreter, die Objekte der Klassen Stackmaschine und Codebuffer haben. Der Interpreter sollte eine Methode liesDatei() haben, die den Codebuffer veranlasst, die Codedatei einzulesen und dann - zu Kontrollzwecken - in der Konsole anzuzeigen.

Ein Interpretieren der Codezeilen ist hier noch nicht vorgesehen.


Schritt 6 - Analyse von Strings

Als Nächstes wollen wir den Stackinterpreter dazu bringen, das zu tun, wozu er hauptsächlich gedacht ist, nämlich die jeweils aktuelle Befehlszeile zu interpretieren.

Der Interpreter ruft zunächst den Codebuffer auf, damit dieser die Textdatei in einem internen Speicher zwischenlagert. Ein Anzeigen in der Stackbefehle in der Konsole ist eigentlich überflüssig; später werden wir das System so erweitern, dass die wichtigsten Daten in einem Java-Applet dargestellt werden.

Hat der Codebuffer die Textdatei mit den Stackbefehlen gelesen, muss sie "die jeweils aktuelle" Befehlszeile an den Interpreter übergeben. Wir benötigen also eine sondierende Methode, die genau dies macht. Die sondierende Methode könnte zum Beispiel heißen

public String naechsterBefehl()

Der Interpreter erhält also einen String von dem Codebuffer und muss dann den String analysieren. Wenn der String beispielsweise den Wert "push 3.14" hat, so muss der Interpreter erkennen, dass es sich a) um den push-Befehl handelt und dass b) der Wert 3.14 gepusht werden soll. Für diese Analyse-Arbeit müssen wir uns jetzt ein wenig mit Strings beschäftigen. Lesen Sie dazu bitte den Lexikon-Eintrag zur Klasse String durch.

Erkennen des Befehls

Ein Befehl wie "push 3.14" liegt zunächst als ein einfacher String vor. Wie kann nun erkannt werden, dass es sich um den push-Befehl handelt und nicht um den add- oder mul-Befehl? Da es nur fünf verschiedene Stackmaschinen-Befehle gibt (zur Zeit jedenfalls), reicht es aus, den ersten Buchstaben des Strings zu analysieren. Handelt es sich dabei um ein "p", so kann nur der push-Befehl gemeint sein. Wenn ein etwas gedankenloser Programmierer allerdings einen Befehl wie "padd" in die Textdatei geschrieben hat, hat er eben Pech gehabt; auch dieser Befehl würde als "push" interpretiert. Vielleicht sollte man also doch nicht nur auf den ersten Buchstaben achten.

Ideal hierfür ist die Methode startsWith(String pre). Sie können in Ihren Java-Quelltext also eine if-Abfrage wie

if (befehl.startsWith("push")) ...

einbauen. Entsprechend verfahren Sie für die anderen vier Stackmaschinenbefehle add, sub, mul und divi.

Richtig interessant wird es aber nur beim push-Befehl, denn hier müssen wir auch noch das Zahlen-Argument aus dem Befehls-String heraus holen und als double-Zahl zurück liefern, damit die Stackmaschine diese Zahl pushen kann.

Erkennen des Arguments

Dieses Problem werden wir in zwei Schritten lösen. Zunächst müssen wir den Teilstring "3.14" aus dem String "push 3.14" extrahieren, und anschließend müssen wir den Teilstring "3.14" in die double-Zahl 3.14 übersetzen. Denn die Methode push() der Klasse Stack erwartet ja eine double-Zahl als Parameter und nicht einen String.

Herauslösen des Arguments

Da nicht bekannt ist, welche Zahl als Argument des push-Befehls verwendet wird, kann man den Befehl startsWith() oder das analoge endsWith() natürlich nicht verwenden. Was gibt es sonst noch für interessante String-Befehle?

public String substring(int beginIndex)

scheint ein solcher Befehl zu sein. Dieser Befehl extrahiert einen Substring aus dem String, und zwar beginnend mit der als Parameter übergebenen Anfangsposition. Wenn wir also die Zeile

"push 3.14"

näher untersuchen, stellen wir Folgendes fest:

p u s h _ 3 . 1 4
0 1 2 3 4 5 6 7 8

Die zu extrahierende Zahl beginnt bei Index 5. Also müsste man im Quelltext der entsprechenden Stackmaschinen-Methode schreiben können:

argString = befehl.substring(5);

Der String argString müsste bei "push 3.14" dann den Wert "3.14" haben.

Konvertieren des Arguments

Jetzt haben wir das Argument des push-Befehls extrahiert, es liegt aber immer noch als String vor. Als nächstes müssen wir den String "3.14" (bzw. das jeweilige Argument) in eine Zahl vom Typ double konvertieren (umwandeln). Bei der Suche nach einer geeigneten Konvertierungs-Methode werden wir in der Dokumentation der Klasse String nicht fündig. Versuchen wir es einmal mit der Klasse Double. Diese Klasse stellt wichtige Routinen zum Umgang mit double-Zahlen zur Verfügung, so zum Beispiel auch parseDouble(), welches im Return-Befehl einer sondierenden Methode eingesetzt werden kann.

public double gibZahl(String s)
{
   return Double.parseDouble(s);
}

Die Klasse String (Sun-Dokumentation)

Lexikon-Eintrag zur
Klasse String

Schritt 7 - Strategische Überlegungen II

Eine strategische Überlegung haben wir ja bereits hinter uns gebracht, als wir verschiedene Möglichkeiten durchspielten, in welchem Verhältnis die Klassen Stackinterpreter, Stackmaschine und Codebuffer zueinander stehen sollen. Ich habe Sie dann ja dazu überredet, die Variante zu wählen, bei der der Stackinterpreter Objekte vom Typ Stackmaschine und Codebuffer HAT.

Nun folgt eine weitere strategische Überlegung. Wer bzw. welche Klasse soll eigentlich die Codeanalyse durchführen. Hier gibt es wieder zwei Möglichkeiten.

Möglichkeit 1

Der Codebuffer gibt den jeweils aktuellen Befehl als einfachen String an den Interpreter zurück, und die Analysearbeit wird im Interpreter geleistet.

Möglichkeit 2

Der Codebuffer selbst analysiert den Befehlsstring und gibt dann eine Befehlsnummer sowie gegebenenfalls (falls es sich um den push-Befehl handelt) das Argument des Befehls an den Interpreter zurück.

Nachdem ich mit meinen Kursen jahrelang die Möglichkeit 1 realisiert habe, möchte ich nun (Mai 2008) zum ersten Mal die zweite Möglichkeit ausprobieren. Das formuliere ich gleich mal als nächste Aufgabe:

Übung 21.7 (4 Punkte)

Erstellen Sie jetzt die Klasse Codebuffer zwei Methoden:

public int gibNaechstenBefehl()
public double gibArgument()

Die Methode gibNaechstenBefehl() analysiert die aktuelle Codezeile* und gibt eine int-Zahl zurück, und zwar

1 = push, 2 = add, 3 = sub, 4 = mul und 5 = div.

Wurde kein gültiger Befehl erkannt, so soll eine negative Zahl zurück geliefert werden, als Fehlermeldung quasi.

Die Methode gibArgument() liefert das Zahlenargument des push-Befehls zurück. Wird gibArgument() aufgerufen, wenn zum Beispiel ein add-Befehl erkannt wurde, so soll der Zahlenwert 0 zurück geliefert werden. Es ist aber Sache des Programmierers des Stackinterpreters, dafür zu sorgen, dass dieser Fall gar nicht eintreten kann. Vielmehr muss der Interpreter so implementiert werden, dass gibArgument() nur dann aufgerufen wird, wenn der push-Befehl erkannt wurde.

*Sie müssen Ihren Codebuffer um ein Attribut aktuell erweitern. Dieses Attribut speichert, welche der Befehlszeilen gerade die aktuelle Befehlszeile ist. Wurde gibNaechstenBefehl() aufgerufen, so muss aktuell inkrementiert werden, so dass beim nächsten Aufruf von gibNaechstenBefehl() tatsächlich der Befehl in der nächsten Codezeile analysiert wird.


Weiter mit Teil 3 der Folge 21

Diese HTML-Seite wurde erstellt von Ulrich Helmich am 21. Juli 2006 und sehr stark überarbeitet am 7. Mai 2008.




(C) Ulrich Helmich, Mai 2008





IMPRESSUM