Von Enno Runne und Hendrik C.R. Lock
StreamTokenizer
,
Hashtable
und Enumeration
der Standardbibliothek
behandeln, die sich im praktischen Einsatz oft als sehr nützlich erweisen.
Sie bieten Funktionalität, deren Programmierung für den Benutzer
zumeist mühsam und fehlerträchtig wäre.
StreamTokenizer
- Eingabe scheibchenweiseStreamTokenizer
dient dazu, Eingaben aus
Strömen zu lesen und diese in ihre einzelnen Symbole (sogenannte
Tokens) zu zerlegen. Ein Token kann dabei ein Wort, eine Zahl,
ein Sonderzeichen oder das Zeilenende sein.
Die Klasse StreamTokenizer
lässt sich flexibel konfigurieren. Ihr kann z.B. mitgeteilt werden,
welche Zeichen den Text in Token zertrennen, und welche Zeichen
überspringbare Leerzeichen sind.
Für unsere Adresskartei führen wir ein selbsterklärendes Textdateiformat ein, mit dem sich die Daten auch leicht in anderen Programmen verwenden lassen. Textdateien haben außerdem den Vorteil, dass sie mit jedem normalen Editor bearbeitet werden können. In diesem Format füllt eine Adresse eine einzige Zeile, in der die Reihenfolge der Felder (z.B. Name, Adresse, Alter) beliebig ist. Außerdem dürfen Felder unbenutzt sein. Gültige Zeilen sehen dann so aus:
name='Graf Hardenberg' vorname = 'Otto' alter=103 alter=1 vorname='Mathias' name= 'Müller' vorname= 'Erika' name='Müller' vorname='Martin' alter=45Das Format ist also auch tolerant bezüglich Leerraum. Ein Feld beginnt immer mit
feldname=
. Bei Zeichenketten, die
Leerzeichen enthalten, verwenden wir die einfachen Anführungszeichen.
Als kleines Extra
sollen auch einzeilige Kommentare im Java-Stil erlaubt sein.
Zeilen, die also mit //
beginnen, bleiben unbeachtet.
Der StreamTokenizer
erledigt die meistem dieser
Anforderungen quasi standardmäßig. So werden
Füllzeichen automatisch
überlesen, Anführungszeichen erkannt und
die enthaltenen Zeichenketten als Token
zurückgeliefert. Sogar die gewünschten Kommentare sind
frei verfügbar. (Übrigens werden sogar mehrzeilige Kommentare mit
/* */
auf Wunsch erkannt und ausgeblendet.)
Erkennung des Zeilenendes | eolIsSignificant(false); |
|
// -Kommentare |
slashSlashComments(false); |
|
/* */ -Kommentare |
slashStarComments(false); |
|
Wort-Zeichen | wordChars('a', 'z');
| |
Zwischenraumzeichen | whiteSpaceChars(0, ' '); |
|
einzeiliger Kommentar | commentChar('/'); |
|
Anführungszeichen | quoteChar('\"'); quoteChar('\''); | |
Lesen von Zahlen | parseNumbers(); |
StreamTokenizer
Der StreamTokenizer
besitzt bestimmte
standardisierte Voreinstellungen,
die wir in Tabelle 1 zusammengestellt haben.
Bevor wir nun den Eingabestrom mittels StreamTokenizer
zerlegen, stellen wir noch diejenigen Parameter ein, die von den
Standardwerten abweichen:
void laden(InputStream is) throws IOException, InvalidAgeException { StreamTokenizer st = new StreamTokenizer(is); st.slashSlashComments(true); st.eolIsSignificant(true); st.whitespaceChars('=', '=');Damit stellen wir ein, dass
//
eine Kommentarzeile
einleiten soll. Weiterhin möchten wir das Zeilenende als extra
Zeichen signalisiert bekommen.
Durch whitespaceChars('=', '=')
schleusen wir das
Gleichheitszeichen als zusätzliches Trennzeichen ein -
es trennt Tokens und wird danach einfach überlesen.
Als nächstes können wir nun Token für
Token aus dem Eingabestrom lesen. Beispielsweise ist durch unsere Voreinstellungen
"name
" oder "'Graf Hardenberg'
" jeweils ein Token.
Bild 1
zeigt, wie eine Zeile aus dem Eingabestrom in einzelne
Tokens aufgebrochen wird. Wir empfangen die einzelnen Tokens in
einer Schleife:
int token; String nameNeu = "", vornameNeu = ""; int alt = 0; boolean feldGelesen = false; do { token = st.nextToken(); if (token == st.TT_WORD) { if ("name".equals(st.sval)) { token = st.nextToken(); if (token == '\'') { nameNeu = st.sval; feldGelesen = true; } } if ("vorname".equals(st.sval)) { token = st.nextToken(); if (token == '\'') { vornameNeu = st.sval; feldGelesen = true; } } if ("alter".equals(st.sval)) { token = st.nextToken(); if (token == st.TT_NUMBER) { alt = new Double(st.nval ).intValue(); feldGelesen = true; } } } if ( ( token == st.TT_EOL || token == st.TT_EOF ) && feldGelesen) { Adresse adr = new Adresse(nameNeu ,vornameNeu, alt); einfügen(adr); feldGelesen = false; nameNeu = ""; vornameNeu = ""; alt = 0; } } while (token != st.TT_EOF); }Liest
nextToken()
ein Wort, so
ist die Bedingung token == st.TT_WORD
erfüllt.
Andernfalls kann mit den Konstanten TT_NUMBER
und TT_EOL
bzw TT_EOF
ein numerischer Wert, ein Zeilenende und das Dateiende
erkannt werden.
Falls ein Wort gelesen wurde, testen wir, ob es eines unserer
Schlüsselworte ist.
In jedem diese Fälle lesen wir jeweils das nächste Token.
Einen gewissen Sonderfall, den StreamTokenizer
bereits unterstützt,
sind durch Hochkommata eingeschlossene Zeichenketten.
Ihre Behandlung als Tokens wird durch
die Methode quoteChar('\'')
voreingestellt.
In diesem Fall gilt token=='\''
, aber der Wert
sval
des Stromes enthält die Zeichenkette ohne die Hochkommata.
Durch eine Modifikation der Vergleiche in
if (token=='\'' || token=='\"')
wird es möglich, auch in
".."
eingeschlossene Texte als Tokens zu behandeln.
Im Falle von token==st.TT_NUMBER
wird eine Zahl erwartet.
Da der StreamTokenizer
Zahlen immer
als Double
liest, benutzen wir für die Umwandlung nach
int
den Ausdruck:
new Double(st.nval).intValue();
Weil eine Adresse immer in einer einzigen Zeile stehen soll, signalisiert
das Zeilenende (TT_EOL
) den Beginn eines
neuen Adressdatensatzes,
falls zuvor mindestens ein Feld eingelesen worden ist.
Fehlt eines der Adressfelder, so wird seine
Voreinstellung ""
in die Adresse übertragen.
Und schon haben wir eine flexible Einlesenmethode. Um das Gelesene in Dateien zu speichern, kommen noch zwei kleine Methoden hinzu, durch die Adressen und Karteien im Dateiformat abgelegt werden.
// neu in Klasse Adresse: void speichern(PrintStream ps) throws IOException { ps.println("name='" + name + "' vorname='"+vorname +"' alter="+alter); } // neu in Klasse Adresskartei: void speichern(PrintStream ps) throws IOException { for (Enumeration e=adrTabelle.elements(); e.hasMoreElements(); ) { Object obj = e.nextElement(); if (obj instanceof Adresse) { ((Adresse)obj).speichern(ps); } } }
Mit Hilfe von Enumeration
werden Datensätze
abgeholt und können dann mittels Methode nextElement()
nacheinander aufgezählt werden, was ganz hilfreich für
Schleifenkonstruktionen sein kann. Diese Aufzählungsklasse wird im
übernächsten Abschnitt vorgestellt.
Hashtable
: Schneller Zugriff auf Datensätze
Vector
) speichert Datensätze
(Klasse Object
),
bietet aber keine Suchfunktion.
Um eine bestimmte Adresse auszulesen
braucht man ihre Position
im Vektor.
Oft benötigt man jedoch die Möglichkeit, einen
Datensatz mit Hilfe eines Schlüssels aufzufinden.
Ein solcher Schlüssel kann z.B. bei Adressen aus Namen und Vornamen
bestehen.
Eine einfache Methode zum Wiederauffinden ist die lineare Suche:
der gesamte Vektor wird abgetastet und jeder Datensatz auf
Gleichheit mit dem Schlüssel getestet.
Bei großen Datenmengen ist diese Methode wegen ihrer Ineffizienz nicht
vertretbar, z.B. bei Telefonbüchern.
Im Idealfall wollen wir in sehr wenigen Schritten zum gesuchten Datensatz finden,
und die Anzahl der Schritte sollte dabei möglichst
nicht von der Anzahl der Einträge abhängen.
Die Grundidea bei der Hashtabelle ist es, den Schlüssel als Zahl
(Hashwert) zu codieren,
und mit diesem Wert direkt in die Tabelle einzustechen.
Dort wird dann nochmals
verglichen, ob der vorliegende Datensatz mit dem Schlüssel übereinstimmt.
Wenn nicht, wird eine sogenannte Konfliktauflösung durchgeführt
(siehe Bild 3).
Letztere sucht im Vektor zielgerichtet nach allen Datensätzen mit
gleichem Hashwert, bis der Schlüssel gefunden wird.
Die Hashtabelle funktioniert ansonsten wie ein Vektor,
ihre Funktionsweise wird in Bild 2 und 3 gezeigt.
Die folgenden Programmteile zeigen, wie sich die Standardklasse
Hashtable
in der Adresskartei einsetzen läßt.
class AdressKartei { Hashtable adrTabelle; AdressKartei() // Konstruktor { adrTabelle = new Hashtable(16); } void einfügen(Adresse adr) { adrTabelle.put(adr, adr); } void löschen(Adresse adr) throws ArrayIndexOutOfBoundsException { adrTabelle.remove(adr); }Die Hashtabelle verwaltet einen eigenen Vektor, der mitwächst. Das erste Argument der Einfügeoperation
Object put()
ist der Schlüssel, und das zweite der Datensatz.
In unserem Fall nehmen wir den Datensatz zugleich als Schlüssel.
Entscheidend ist hierbei
die von der Hashtabelle benutzte Methode equals()
,
durch die festgelegt wird,
welche Teile des Datensatzes den Schlüssel bilden.
Wir überschreiben diese Methode in Klasse Adresse
,
da das von Object
geerbte equals()
nämlich die Identität der Objekte (bzw. ihrer Referenzen) testet.
public boolean equals(Object obj) { if (obj instanceof Adresse) { Adresse a = (Adresse)obj; return (a.getKey()).equals(this.getKey()); } return false; } public String getKey() { return name+vorname; }Methode
Object put()
legt also eine Adresse ab.
Ist die Kombination "name+vorname
" bereits abgelegt, so ersetzt es
die gespeicherte Adresse durch die neue, und gibt die alte zurück.
Das bedeutet für die Adresskartei, dass keine zwei Adressen für
gleiche Namen (Schlüssel) gespeichert werden können.
Dies könnte man z.B. durch eine Reihe abgestufter Hashtabellen beheben,
die die verdrängten Adressen aufnehmen.
Weiterhin erbt jedes Objekt eine Methode hashCode()
,
die ebenfalls von Hashtable
benutzt wird.
Wir überschreiben
diese Methode in Klasse Adresse
so, dass der Hashwert aus dem
Schlüssel berechnet wird:
public int hashCode() { return (name+vorname).hashCode(); }Die Suche nach einer Adresse über den Namen besteht nun aus einem einfachen Test mit einem Schlüssel und einem Zugriff. Wir nehmen dabei an, dass
kartei
vom Typ AdressKartei
ist.
Adresse key = new Adresse(name,vorname,0); if (kartei.contains(key)) Adresse a = kartei.get(key);Der Test ist nicht unwichtig, denn
get()
signalisiert das Fehlen des Schlüssels mit dem Rückgabewert
null
.
Enumeration
:
Datenzugriff stückweise
public interface java.util.Enumeration { public abstract boolean hasMoreElements(); public abstract Object nextElement(); }Mit der Deklaration
class A implements Enumerationwird zugesichert, dass Klasse
A
beide Methoden implementiert.
hasMoreElements()
liefert true
,
solange noch nicht alle Elemente aus der Sammlung abgeholt worden sind,
und Object nextElement()
holt ein Element ab.
Wie bei Vector
und Hashtable
kennen wir den Typ der Elemente nicht, so dass hier Object
angenommen wird.
Bei einer Zuweisung Adresse x = (Adresse) sammlung.nextElement()
muss
deshalb explizit auf den Zieltyp (hier Adresse)
umgewandelt werden.
Das Interessante ist nun, dass sowohl Hashtable
als Vector
diese Schnittstelle implementieren.
Dadurch können wir eine Schleife bauen, die die Inhalte
sowohl einer Hashtabelle als auch eines Vektors ausliest.
Dadurch sind wir in der Lage,
einen Vektor gegen eine Hashtabelle auszutauschen, ohne den Schleifencode
ändern zu müssen!
Wir hatten Enumeration
bereits zuvor verwendet:
for (Enumeration e=adrTabelle.elements(); e.hasMoreElements(); ) { Object obj = e.nextElement(); if (obj instanceof Adresse) ((Adresse)obj).speichern(ps); }Die Methode
Enumeration elements()
der Klasse Hashtable
liefert ein Objekt zurück, das Enumeration
implementiert
(Beachten Sie bitte, dass dieser Aufruf im Initialisierungsteil einer
for
-Schleife auftritt).
Das gleiche gilt übrigens auch für Klasse Vector
.
Die Klasse des zurückgegebenen Objektes
kennen wir nicht, und sollen wir auch nicht kennen,
denn nur so kann gewährleistet werden, dass diese Schleife unabhängig von
der tatsächlichen Implementierung der Kartei ist. D.h., eine als
Vektor realisierte Kartei funktioniert hier genauso gut.
Im Schleifenrumpf wird dann eine Adresse nach der anderen aus der Datensammlung
abgeholt und anschließend gespeichert.
Vector
, StreamTokenizer
,
Hashtable
und Enumeration
aufwendigere Programmierarbeiten durch eine selbstgestrickte
Verwaltung von Datensätzen vermeiden.
Bisher geben wir Datensätze über eine recht armselige Schnittstelle
ein. Im kommenden Teil beginnen wir deshalb mit der Programmierung
von grafischen Oberflächen mit Hilfe von Javas AWT
(Abstract Window Toolkit).
Den Karlsruher Java Übersetzer, der übrigens 2-3 mal schneller ist
als der von Sun ausgelieferte Übersetzer, findet man unter
folgender Adresse: http://wwwipd.ira.uka.de/~pizza/.
Die virtuelle Java Maschine und der Sun-Übersetzer
javac
für zahlreiche Zielrechner (SPARC Solaris, x86
Solaris, Windows NT, Windows 95, MacOS) befinden sich unter http://java.sun.com/products.