Die Sprache Java

Werbung
2-1
Die Sprache Java
Die Sprache Java
Dieser Abschnitt erklärt die Sprache Java an einer Reihe von kleinen Beispielen.
Themen
Integer-Daten
Gleitkomma-Daten
Bedingungen
Zeichen und Zeichenketten
Eigene Objekte
Interface
Ein Paket für Unicode
Die Kommandozeile als InputStream
Ein Framework für die Kommandozeile
Dateien als Strom ausgeben
Eine Klasse wiederverwenden
Information über Dateien
Einen Algorithmus verkapseln
Threads
Anweisungen
Die folgenden Anweisungen werden in diesem Abschnitt eingeführt:
break
continue
do-while-Schleife
for-Schleife
if, if-else
import
package
return
switch
Monitor
throw Ausnahme
try-catch-finally Ausnahmebehandlung
while-Schleife
synchronized
System-Klassen
Das Paket java.langsteht implizit zur Verfügung. Folgende Klassen (ohne Unterklassen
von Exception) werden in diesem Abschnitt verwendet:
Boolean
Character
2-2
Double
Exception
Float
Integer
Math
String
StringBuffer
Thread
Die Klasse System wird in den meisten Beispielen zum Zugriff auf Standard-Ein- und
Ausgabe und das Environment etc. verwendet.
Eingabe- und Ausgabe-Klassen
Das Paket java.ioenthält ein System von Klassen zur Eingabe und Ausgabe sowie zum
Umgang mit Dateien. Folgende Klassen (ohne Unterklassen von Exception) werden in
diesem Abschnitt verwendet:
BufferedOutputStream
DataInputStream
File
FileInputStream
FilterInputStream
FilterOutputStream
InputStream
OutputStream
PipedInputStream
PipedOutputStream
PushbackInputStream
SequenceInputStream
StreamTokenizer
StringBufferInputStream
Die Klasse PrintStream wird in den meisten Beispielen zum Zugriff auf Standard- und
Diagnose-Ausgabe verwendet.
Hilfsklassen
Das Paket java.utilenthält einige Klassen zur Lösung spezieller Probleme. Folgende
Klassen werden in diesem Abschnitt verwendet:
Enumeration
Properties
Random
StringTokenizer
Vector
2-3
Integer-Daten
Integer-Daten
Hier betrachten wir die verschiedenen Integer -Typen, Konstanten und Operationen.
int/Rangesillustriert vor allem Bit-Operationen, int/Try zeigt, wie man eine Division
durch Null abfängt.
Für die einfachen Datentypen gibt es auch Klassen. int/Cvt verwendet einige
Klassenmethoden aus der Integer-Klasse zur Umwandlung zwischen Strings und
Integer-Werten.
Typen und Wertbereiche
Java kennt nur Integer mit Vorzeichen, strikt definierten W ertbereichen und
Repräsentierung im Zweierkomplement.
byte
short
int
long
8 Bit
16 Bit
32 Bit
64 Bit
-128
-32768
-2147483648
-9223372036854775808
127
32767
2147483647
9223372036854775807
Konstanten
Konstanten werden mit Präfix 0x oder 0X in Basis 16, mit Präfix 0 in Basis 8 und sonst
dezimal interpretiert; mit Suffix l oder L haben sie den Typ long, sonst int. Dezimale
Konstanten müssen im positiven W ertbereich liegen.
Operationen
Integer-Werte können von und zu Integer- und Gleitkomma-Werten sowie char
umgewandelt werden, aber nicht von und zu boolean.
Es gibt die üblichen Operatoren: Vorzeichen + - ~, Inkrement und Dekrement ++ --,
Arithmetik + - * / %, Vergleiche == != <= >= < >, Bit-Operationen & | ^ << >> sowie
ohne Propagieren des Vorzeichens >>>.
Integer-Operationen erfolgen im Bereich int, und in long nur, wenn wenigstens ein
Operand long ist.
Eine ArithmeticException erfolgt nur bei Division oder Rest-Bildung mit Null.
Berechnung der Wertbereiche — int/Ranges
Rangesgibt die Wertbereiche der Integer-Datentypen aus.
Rangesdemonstriert den Umgang mit Integer -Werten, Bit-Operationen und
Umwandlungen.
2-4
/** A class demonstrating bit operations to determine integer ranges */
class Ranges {
public static void main (String args []) {
byteRange(); shortRange(); intRange(); longRange();
}
/** uses complement operations */
static void intRange () {
System.out.println("int\t" + (~0 >>> 1) + "\t" + (~ (~0 >>> 1)));
}
/** maximum and minimum long value */
static final long maxLong = ~0L >>> 1, minLong = ~ (~0L >>> 1);
static void longRange () {
System.out.println("long\t" + maxLong + "\t" + minLong);
}
/** uses casts and literals */
static void shortRange () {
System.out.println("short\t" + (short)077777 + "\t" + (short)0x8000);
}
/** shifts ones until no further changes occur */
static void byteRange () {
byte i, j = 1;
do { i = j; j = (byte)(i << 1 | 1); } while (j > i);
System.out.print("byte\t" + i);
do { i = j; j = (byte)(i << 1); } while (j < i);
System.out.println("\t" + i);
}
}
erzeugt durch Komplementieren von 0 und Verschieben ohne Propagieren
des Vorzeichens ein maximales und durch erneutes Komplementieren ein minimales
Bitmuster. + dient zum Verketten von Zeichenketten. Integer -Operanden werden dabei
in Text umgewandelt.
intRange()
definiert maxLong und minLong als Klassenvariablen,final erlaubt keine weitere
Zuweisung. Zur Initialisierung wird ein long-Literal und daher long-Arithmetik
verwendet.
static
beruht auf int-Literalen, die explizit in short umgewandelt werden. Die
folgende Lösung für den minimalen W ert ist in C aber nicht in Java möglich:
shortRange()
(short) ~ ((unsigned short) ~0 >> 1)
verwendet do-while-Schleifen. byte kann zwar mit int initialisiert werden,
aber bei der Zuweisung ist eine explizite Umwandlung nötig.
byteRange()
2-5
Ausnahmebehandlung — int/T ry
Try dividiert 6 durch die Anzahl der Kommandoargumente.
Try demonstriert, wie man eine Exception abfängt.
/** A class demonstrating an arithmetic exception */
class Try {
/** divides 6 by the number of arguments */
public static void main (String args []) {
try {
System.out.println(6 +"/"+ args.length +" is "+ 6/args.length);
} catch(Exception e) {
System.err.println(e.getClass().getName() +": "+ e.getMessage());
e.printStackTrace();
} finally {
System.err.println("done");
}
}
}
erhält die Kommandoargumente (ohne java und den Klassennamen) als
konstante Zeichenketten im Vektor args[].
main()
Jeder Vektor hat eine Komponente .length, die die Anzahl der Elemente enthält.
kontrolliert einen Block, dem catch-Blocks folgen. Passiert ein Fehler, wird das
resultierende Exception-Objekt der ersten catch-Klausel übergeben, die es als
Argument empfangen kann. Abschließend wird — mit oder ohne Fehler und falls
vorhanden — der finally-Block ausgeführt. Danach geht es nach try weiter.
try
Auch falls try mit einer Sprunganweisung ( break, continue, return) verlassen wird,
findet finally statt, nicht aber die auf try folgende Anweisung.
ist ein Throwable, das abgefangen werden sollte und das dann untersucht
werden kann. Hier sieht man etwa folgendes:
Exception
$ java Try
Exception: / by zero
java.lang.ArithmeticException: / by zero
at Try.main(Try.java:10)
done
2-6
Umwandlungen — int/Cvt
Cvt wandelt seine dezimalen Argumente in den Radix um, der als erstes, dezimales
Argument angegeben ist.
Cvt illustriert statische Methoden der Integer-Klasse, mit der auch Integer-Werte als
Objekte verpackt werden können.
/** A class using static methods from the Integer class */
class Cvt {
/** converts decimal args[1..] to decimal radix args[0] */
public static void main (String args []) {
if (args.length <= 1) {
System.err.println("usage: Cvt radix number ...");
System.exit(1);
}
try {
int radix = Integer.parseInt(args[0]);
if (radix<2 || radix>36) throw(new Exception("bad radix"));
for (int n = 1; n < args.length; ++ n) {
int arg = Integer.parseInt(args[n]);
switch (radix) {
case 8: if (arg != 0) System.out.print("0");
case 10: break;
case 16: System.out.print("0x"); break;
default: System.out.print(radix + "#");
}
System.out.println(Integer.toString(arg, radix));
}
} catch(Exception e) {
System.err.println(e.getClass().getName() +": "+ e.getMessage());
System.exit(1);
}
}
}
Die if-Anweisung stammt aus C; allerdings muß die Bedingung boolean sein.
Vektoren wie args[] werden ab Null indiziert — mit IndexOutOfBoundsException.
Vereinbarungen können beliebig mit Anweisungen gemischt werden. Der Stil ist
ähnlich zu C, aber einfacher.
Die throw-Anweisung verursacht eine Ausnahme. new erzeugt hier das ExceptionObjekt für die nachfolgende catch-Klausel.
Bei der for-Schleife kann eine lokale Definition stehen oder es können mehrere
Initialisierungen durch Komma getrennt angegeben werden; auch im Inkrement-Teil
kann Komma verwendet werden. Sonst gibt es keinen Komma-Operator .
Die switch-Anweisung wählt mit einem char- oder Integer-Wert (aber nicht mit long)
eine case-Marke oder default: in einem Block. Der Vergleich erfolgt als int.
entspricht atoi(), Integer.toString() erlaubt Radix 2 bis 36 (und
crasht sonst!). Beides sind Klassenmethoden.
Integer.parseInt()
2-7
Gleitkomma-Daten
Gleitkomma-Daten
Hier betrachten wir die beiden Gleitkomma-Typen, Konstanten und Operationen.
float/Rangeszeigt den Wertbereich und verwendet dazu auch Klassenvariablen aus den
Double- und Float-Klassen sowie die Klassenmethode Math.sqrt() zur Erzeugung
einer not a number. float/Qgllöst quadratische Gleichungen und demonstriert
Umwnadlungen und Gleitkomma-Arithmetik.
Typen und Wertbereiche
Java verwendet IEEE-754 Gleitkomma-Werte mit strikt definierten, symmetrischen
Wertbereichen.
float
double
32 Bit
64 Bit
Mantisse
1 .. 2^24
1 .. 2^53
Exponent zu Basis 2
-149 .. 104
-1045 .. 1000
Außer Zahlenwerten gibt es +0.0 und -0.0, positiv- und negativ-unendlich sowie not a
number (NaN).
Konstanten
Konstanten bestehen aus dezimaler Mantisse mit Dezimalpunkt und Exponent mit
Präfix e oder E und optionalem Vorzeichen. Viele Teile können entfallen, allerdings muß
ein Dezimalpunkt oder ein Exponent vorhanden sein.
Nur mit Suffix f oder F haben Konstanten den Typ float. Mit oder ohne Suffix d oder D
sind sie sonst double.
Operationen
Gleitkomma-Werte können von und zu Integer- und Gleitkomma-Werten sowie char
umgewandelt werden, aber nicht von und zu boolean.
Es gibt die üblichen Operatoren: Vorzeichen + -, Inkrement und Dekrement ++ --,
Arithmetik + - * / % und Vergleiche == != <= >= < >. +0.0 und -0.0 sind gleich,
liefern aber bei Division positiv- oder negativ-unendlich. == liefert false wenn NaN
beteiligt ist, != liefert true, beides sogar wenn NaN mit NaN verglichen wird.
Operationen erfolgen im Bereich float, und in double nur, wenn wenigstens ein
Operand double ist.
Gleitkomma-Operationen verursachen keine Ausnahmen.
Berechnung der Wertbereiche — float/Ranges
Rangesgibt die Wertbereiche der Gleitkomma-Datentypen und die Darstellung der
speziellen Werte aus.
Rangesdemonstriert den Umgang mit Gleitkomma-W erten und Zugriff auf die Klassen
2-8
Double, Float
und Math.
/** A class demonstrating arithmetic operations to determine float ranges */
class Ranges {
public static void main (String args []) {
floatRange(); doubleRange(); special();
}
/**
* uses multiplication and division and the Double class
* max is too small by half, Double.MIN_VALUE prints as zero
*/
static void doubleRange () {
double min = 1, max = 1;
for (double d = 1; d != 0.; min = d, d /= 2.)
;
for (double d = 1, n = d * 2.; d != n; max = d, d = n, n *= 2.)
;
System.out.println("double\t"+ min +"\t"+ max);
System.out.println("Double\t"+ Double.MIN_VALUE
+"\t"+ Double.MAX_VALUE);
}
/** uses the Float class */
static void floatRange () {
System.out.println("float\t"+ Float.MIN_VALUE
+"\t"+ Float.MAX_VALUE);
}
/** operations with infinity and NaN */
static void special () {
System.out.println("infinity\t"+ 1/-0. +"\t"+ 1/+0.);
System.out.println("NaN\t"+ Math.sqrt(-1.));
}
}
doubleRange() erzeugt durch wiederholte Division den minimalen und durch
wiederholte Multiplikation den maximalen W ert.
Die Klassen Double und Float dienen zur Repräsentierung von double- und floatWerten als Objekte. Man kann auch Klassenvariablen zur Ausgabe der Wertbereiche
verwenden.
special()
zeigt, daß sich +0.0 und -0.0 unterschiedlich verhalten.
enthält ausschließlich Klassenmethoden für die üblichen mathematischen
Funktionen, darunter auch abs(), max(), und min() für verschiedene Datentypen sowie
PI und E.
Math
$ java Ranges
float
1.4013e-045
3.40282e+038
double 4.94066e-324
8.98847e+307
Double 0
1.79769e+308
infinity
-1.#INF 1.#INF
NaN
-1.#IND
Das Maximum für double ist offenbar um den Faktor 2 zu klein, Double.MIN_VALUE wird
falsch gedruckt.
2-9
Lösung einer quadratischen Gleichung — float/Qgl
Qgl löst die quadratische Gleichung a x^2 + b x + c = 0, deren Koeffizienten auf der
Kommandozeile angegeben werden.
Qgl zeigt Umwandlungen mit Hilfe der Float-Klasse sowie Gleitkomma-Arithmetik.
/** A class to solve a quadratic equation */
class Qgl {
/** takes coefficients from the command line and solves equation */
public static void main (String args []) {
if (args.length != 3) {
System.err.println("usage: Qgl a b c");
System.exit(1);
}
try {
double a = Float.valueOf(args[0]).floatValue();
double b = Float.valueOf(args[1]).floatValue();
double c = Float.valueOf(args[2]).floatValue();
double d = b * b - 4. * a * c;
if (d < 0.) throw(new Exception("no real solutions"));
d = Math.sqrt(d);
double x1 = (-b + d) / (2. * a);
double x2 = (-b - d) / (2. * a);
System.out.println("solutions: "+ x1 +" "+ x2);
} catch(Exception e) {
System.err.println(e.getClass().getName() +": "+ e.getMessage());
System.exit(1);
}
}
}
Die Umwandlung von String zu float ist unnötig mühsam, denn Float.valueOf()
erzeugt zunächst ein Float-Objekt, dessen Wert dann mit floatValue() ermittelt wird.
.
Das Objekt verschwindet später durch garbage collection
Das Beispiel zeigt, daß man Anweisungen und Vereinbarungen mischen kann, damit
Variablen möglichst dicht bei der Benutzung definiert werden.
Lokale Variablen können offenbar keine Attribute wie final oder static besitzen.
2-10
Bedingungen
Bedingungen
Bedingungen können nur mit dem Datentyp boolean ausgedrückt werden — ein
impliziter Vergleich mit Null wie in C ist nicht zulässig.
hat die vordefinierten Konstanten true und false, deren Namen theoretisch
nicht reserviert sind.
boolean
Operationen
boolean-Werte
können nicht umgewandelt werden.
Es gibt die üblichen Operatoren: Negation !, Vergleiche == !=, logische Operationen &
| ^ und logische Operationen mit vorzeitigem Abbruch && ||.
Vergleiche bei anderen Datentypen liefern true oder false als Resultat.
boolean-Operationen
verursachen keine Ausnahmen.
2-11
Umwandlungen — boolean/Cvt
Cvt wandelt die W erte auf seiner Kommandozeile um und vergleicht sie mit true.
Cvt demonstriert den Umgang mit boolean-Werten und Zugriff auf die Klasse Boolean.
/** A class to play with boolean values */
class Cvt {
/** converts boolean arguments */
public static void main (String args []) {
if (args.length == 0) {
System.err.println("usage: Cvt true FALSE ...");
System.exit(1);
}
boolean truth = Boolean.TRUE.booleanValue();
for (int n = 0; n < args.length; ++ n) {
Object arg = Boolean.valueOf(args[n]);
if (arg != Boolean.TRUE)
System.out.println(arg +"\t"+ truth +"\t"+
(arg.equals(Boolean.TRUE) ? "ok" : "no"));
}
}
}
verkapselt boolean-Werte als Objekte. Boolean.TRUE und Boolean.FALSE sind
true und false als Objekte. Mit booleanValue() kann man den Wert aus einem Objekt
holen.
Boolean
liefert ein Boolean-Objekt zu einem als String repräsentierten Wert.
"true", beliebig groß- oder kleingeschrieben, liefert eine Verkapselung von true, alles
andere führt zu false.
Boolean.valueOf()
Boolean ist, wie alle Klassen, eine Unterklasse von Object, also kann man das Resultat
an eine Object-Variable zuweisen.
Vergleicht man Objekte mit == oder !=, wird immer auf Identität verglichen. equals()
ist eine Methode, die in der Klasse Object ebenfalls auf Identität vergleicht. Andere
Klassen erben die Methode zwar, ersetzen sie aber normalerweise durch einen
inhaltlichen Vergleich. arg ist zwar als Object definiert, verweist aber auf ein BooleanObjekt — deshalb wird beim Aufruf arg.equals() der Vergleich aus der Klasse Boolean
verwendet, der durchaus im Gegensatz zum Resultat von != stehen kann.
Bei String-Verkettung mit + werden Objekte implizit jeweils mit toString()
umgewandelt. Für boolean-Werte entstehen die Texte "true" und "false".
2-12
Zeichen und Zeichenketten
Zeichen und Zeichenketten
Dieser Abschnitt diskutiert Zeichen und Zeichenketten sowie Klassen zum Aufbau und
zur Zerlegung von Zeichenketten und Verfahren zur Ein- und Ausgabe.
char/Wc illustriert die Klassifikation einzelner Zeichen und Unicode-Werte sowie den
Umgang mit Objekten einer eigenen Klasse. char/Propdemonstriert die Properties, die
an Stelle der UNIX-Environmentvariablen verwendet werden. char/Cvtzeigt, wie man
Textdateien in die lokalen Systemkonventionen umwandelt.
Als Hilfsklassen werden UTFInputStream und UTFOutputStream zum Lesen und
Schreiben von Unicode im UTF-Format implementiert, die ein eigenes Paket bilden.
char/Calcbewertet Zeilen mit Gleitkomma-Ausdrücken. char/Exprerweitert die Klasse
Calc, um auch Ausdrücke von der Kommandozeile zu bewerten.
char
Java verwendet den Unicode-Standard. Der Typ char hat 16 Bit und gilt nicht als
Integer-Typ, kann aber in arithmetische Werte und umgekehrt umgewandelt werden.
Davon abgesehen können char-Werte nur verglichen und zugewiesen werden.
Die Character-Klasse hat insbesondere Klassenmethoden zur Zeichenklassifikation, die
in char/Wc verwendet werden.
Zeichenkonstanten bestehen aus einem Zeichen in einfachen Anführungszeichen.
Dabei gibt es (nur!) die folgenden, auch in C üblichen Ersatzdarstellungen:
\b \f \n \r \t \\ \’ \" \o \oo \ooo
mit 1 bis 3 oktalen Ziffern für einen 8-Bit-W ert.
Jedes Zeichen der Programmquelle kann hexadezimal im Unicode durch \uxxxx
dargestellt werden; allerdings findet die Ersetzung sehr früh statt und ist keine
Alternative zu \n etc.
String
Java verwendet die Klasse String für konstante Zeichenketten. Sie hat insbesondere
Methoden im Stil der in C üblichen String-Funktionen.
Anders als in C sind Strings nicht null-terminiert und die Speicherverwaltung ist sicher.
Zeichenkettenkonstanten sind String-Objekte und bestehen aus Zeichen in
Doppelanführungszeichen mit den gleichen Ersatzdarstellungen wie für charKonstanten.
StringBuffer
Java verwendet die Klasse StringBuffer für Operationen mit Zeichenketten. Sie kann
insbesondere ihre Objekte auch als String-Objekte abliefern.
2-13
Verkettung und toString
Der Operator + erlaubt dynamische Verkettung von String-Werten und liefert ein
String-Resultat, wenn wenigstens ein Argument ein String ist. Das andere Argument
wird entsprechend umgewandelt.
definiert die Methode toString(), die von + implizit zur Umwandlung von
Objekten verwendet wird. In der Regel wird diese Methode so definiert, daß beliebige
Objekte sinnvoll dargestellt werden.
Object
StringTokenizer und StreamTokenizer
Ein java.util.StringTokenizer-Objekt zerlegt einen String-Wert ähnlich wie
strtok(), jedoch in einer saubereren Implementierung.
Ein java.io.StreamTokenizer liefert einen InputStream als Folge von Symbolen.
char/Calcdemonstriert, wie gut sich diese Klasse zur Analyse von arithmetischen
Ausdrücken eignet.
Eingabe und Ausgabe von Unicode
Zur Eingabe und Ausgabe werden immer die InputStream- und OutputStream-Objekte
verwendet. InputStream und OutputStream bearbeiten nur byte. Aus byte-Vektoren
kann man String-Objekte konstruieren, wobei allerdings nur 8 Bit berücksichtigt
werden.
DataInputStream
und DataOutputStream bearbeiten auch char und String, jedoch nur
als 16 Bit.
Es gibt eine 8-Bit Darstellung von Unicode, das sogenannte UTF-Format, aber die
Methoden readUTF() und writeUTF() bearbeiten nur ein Format, bei dem die Anzahl
der Zeichen dem Inhalt des Strings vorausgeht; außerdem ist readUTF() defekt.
Zwei eigene Klassen, UTFInputStream und UTFOutputStream realisieren Methoden, mit
denen UTF-Dateien als Text gelesen und geschrieben werden können. Zum Test dient
die Datei unicode, die von Plan 9 stammt.
2-14
Eigene Objekte
Eigene Objekte
Von einer Klasse erzeugt man normalerweise Objekte, die dann Instanzvariablen
besitzen.
Objekte werden dynamisch mit dem V orzeichenoperator new erzeugt. Dabei wird ein
Konstruktor aufgerufen, der die Instanzvariablen initialisiert.
In Methoden greift man vor allem auf die Instanzvariablen des Empfänger-Objekts zu.
Zeilen, Worte und Zeichen zählen — char/Wc
Wc zählt die Zeilen, Worte und Zeichen in der Standard-Eingabe oder in Dateien. Der
Schalter -u verlangt, daß die Eingabe im UTF-Format interpretiert wird.
Wc illustriert den Umgang mit Zeichen und String-Objekten sowie den Einsatz von
eigenen Klassen.
import java.io.*;
import java.util.*;
import lib.utf.*;
// StringTokenizer
// UTFInputStream
/** A class to implement the `wc’ command */
public class Wc {
/** count and display for stdin or argument files; -u if UTF */
static public void main (String args []) {
try {
if (args.length == 0)
// stdin
System.out.println(new Wc(System.in));
else if (! "-u".equals(args[0])) {
// file ...
int lines = 0, words = 0, chars = 0;
for (int n = 0; n < args.length; ++ n) {
Wc f = new Wc(new FileInputStream(args[n]));
System.out.println(f +"\t"+ args[n]);
lines += f.l; words += f.w; chars += f.c;
}
if (args.length > 1)
System.out.println(lines+"\t"+words+"\t"+chars+"\ttotal");
} else if (args.length == 1)
// -u
System.out.println(new Wc(new UTFInputStream(System.in)));
else {
// -u file ...
int lines = 0, words = 0, chars = 0;
for (int n = 1; n < args.length; ++ n) {
Wc f = new Wc(new
UTFInputStream(new FileInputStream(args[n])));
System.out.println(f +"\t"+ args[n]);
lines += f.l; words += f.w; chars += f.c;
}
if (args.length > 2)
System.out.println(lines+"\t"+words+"\t"+chars+"\ttotal");
}
} catch(Exception e) {
System.err.println(e.getClass().getName() +": "+ e.getMessage());
System.exit(1);
}
}
Es gibt vier Möglichkeiten: keine Argumente, der Schalter -u, Dateinamen oder -u und
Dateinamen.
In jedem Fall wird ein Wc-Objekt erzeugt, das das Zählen für eine Datei übernimmt.
2-15
Ein Wc-Objekt kann aus einem InputStream wie System.in konstruiert werden. Um ein
Objekt zu erzeugen, wird nach dem Vorzeichen-Operator new der Aufruf des
Konstruktors angegeben.
gibt Objekte aus, indem es die in Object erstmalig definierte Methode
toString() verwendet. Für Wc-Objekte wird sie so ersetzt, daß sie die verschiedenen
Zahlen liefert.
println()
Zeichenketten wie "-u" sind String-Objekte. Die Methode equals() dient für StringObjekte zum Text-Vergleich.
Wenn viele Dateien angegeben sind, wird jeweils ein Wc-Objekt pro Datei erzeugt und
mit println() dargestellt. Die individuelle Statistik wird aufsummiert und eventuell
gezeigt.
In einer Klassenmethode kann man auf die Instanzvariablen eines Objekts im Stil von
Strukturkomponenten wie f.l zugreifen. Anderswo geht das nur, wenn der Zugriff
nicht eingeschränkt ist. In einer Methode kann der Name this für das EmpfängerObjekt entfallen.
Zum Lesezugriff auf eine Datei konstruiert man ein FileInputStream-Objekt aus dem
Dateinamen. Dabei kann eine FileNotFoundException passieren.
Ist der Schalter -u angegeben, konstruiert man Wc-Objekte aus einem UTFInputStream,
der einem InputStream und damit auch einem FileInputStream aufgepfropft werden
kann.
Konstruktoren
Zur Erzeugung von Objekten gibt es Konstruktoren — Methoden mit dem gleichen
Namen wie die Klasse. Sie sollten die Instanzvariablen initialisieren, die Java zunächst
auf Nullwerte setzt.
Lokale Variablen werden nicht initialisiert, aber der Compiler kontrolliert, daß eine
Variable nicht benutzt wird, bevor sie einen definierten W ert besitzt.
Instanzvariablen werden wie Klassenvariablen definiert — innerhalb der Klasse,
außerhalb der Methoden, und mit den Attributen public, protected oder private für
Zugriffsschutz sowie final zum Schutz gegen Zuweisung (analog zu const in C). Ohne
Zugriffsschutz-Attribut ist wieder friendly-Zugriff innerhalb einer Quelle und eines
Pakets möglich.
2-16
/** instance’s lines, words, and characters */
protected int l, w, c;
/** create instance, count every character */
public Wc (InputStream in) throws IOException {
int ch;
for (boolean inword = false; (ch = in.read()) != -1; ++ c)
if (Character.isSpace((char)ch))
switch (ch) {
case ’\r’:
switch (ch = in.read()) {
case ’\n’:
++ c;
// \r \n
case -1:
break;
// \r eof
default:
if (! (in instanceof PushbackInputStream))
in = new PushbackInputStream(in);
((PushbackInputStream)in).unread(ch);
}
// \r
case ’\n’:
++ l;
// \n
default:
inword = false; // white
}
else if (! inword) {
// nonwhite
++ w; inword = true;
}
}
liefert ein Byte von einem InputStream als positiven int-Wert oder -1 am DateiEnde. Ein Eingabe-Fehler führt zu einerIOException, die immer explizit behandelt
werden muß, da sie nicht von RuntimeException abstammt.
read()
ist eine der Klassenmethoden zur Zeichenklassifikation in Character. Andere
Methoden beschäftigen sich mit Ziffern und Buchstaben, allerdings nur im Latin-1Bereich.
isSpace()
ist ein Vergleichsoperator, mit dem man untersucht, ob ein Objekt zu einer
explizit angegebenen Klasse gehört. Eigentlich wird geprüft, ob eine Zuweisung
möglich ist, das heißt, Unterklassen und Interface-Namen sind möglich.
instanceof
kann auf einen InputStream aufgepfropft werden, damit ein
einzelnes Zeichen mit unread() zurückgestellt werden kann.
PushbackInputStream
Die Umwandlung von einer Oberklasse wie InputStream in eine Unterklasse wie
wird zur Laufzeit überprüft. Der Aufruf von unread() ist
syntaktisch spannend, aber leider typisch.
PushbackInputStream
Man beachte, daß im weiteren V erlauf in.read() dann auf die PushbackInputStreamMethode verweist und das zurückgestellte Zeichen liest.
Konstruktor-Aufrufe sind implizit verkettet — wenn am Anfang des Konstruktors kein
Aufruf super() oder this() steht, wird implizit super() ohne Argumente aufgerufen,
und dieOberklasse muß dann einen entsprechenden Konstruktor bereitstellen.
Wenn ein Konstruktor nicht public vereinbart wird, schränkt man ein, wer Objekte
erzeugen kann: bei private gibt es keine Objekte, bei protected gibt es nur
Unterklassen-Objekte.
2-17
/** create instance, count runes */
public Wc (UTFInputStream in) throws IOException {
String s;
while ((s = in.get()) != null) {
c += s.length();
w += new StringTokenizer(s).countTokens();
switch (s.charAt(s.length()-1)) {
case ’\n’: case ’\r’: ++ l;
}
}
}
/** format information */
public String toString () {
return l +"\t"+ w +"\t"+ c;
}
}
Konstruktoren können wie alle Methoden für verschiedene Parametertypen vereinbart
werden. Außerdem wird zusätzlich zum Overloadingleider noch implizit umgewandelt,
um eine Methode zu finden.
lib.utf.UTFInputStream
liefert mit get() eine Zeile als String, inklusive der
Zeilentrenner.
liefert die Anzahl Zeichen in einem String. charAt() dient zum Zugriff auf die
einzelnen Zeichen, mit Index ab Null.
length()
Ein StringTokenizer liefert aus einem String die einzelnen Worte, wobei man die
Trennzeichen auch festlegen kann. countTokens() ist die effiziente Methode zum
Zählen der vorhandenen Worte.
sollte so ersetzt werden, daß ein Objekt der eigenen Klasse zum Beispiel
für println() beschrieben wird.
toString()
2-18
Interface
Interface
Java hat nur einfache Vererbung für Klassen. Daneben gibt es aber InterfaceDeklarationen, die das Wort interface an Stelle von class verwenden und in denen
nur Methoden und Klassenvariablen vereinbart werden. Zu einer Klasse können mit
implements Interfaces angegeben werden; dann müssen deren Methoden in der Klasse
implementiert werden.
Ein Interface kann dann als Typ einer Variablen verwendet werden, an die alle Objekte
zugewiesen werden können, deren Klassen das Interface implementieren. In dieser
Beziehung wird ein Interface wie eine Klasse verwendet.
Dies ist eine Art von Mehrfachvererbung, die billig zu implementieren und flexibel
genug ist, um das Problem zu lösen, daß man zwar eine eigene Klasse konstruieren,
aber doch Leistungen aus einem anderen Bereich anbieten oder nutzen möchte. Das
Vorbild war das protocol in Objective C, das allerdings auch dynamisch verwendet
werden kann.
Properties — char/Prop
Java verwendet sogenannte Properties an Stelle der bei UNIX üblichen
Environmentvariablen, damit der Zugriff eingeschränkt und auf jeder Plattform
implementiert werden kann. javacholt seine Fehlermeldungen aus einer Datei mit
Properties.
Prop zeigt Namen und Werte aller System-Properties oder Werte zu explizit
angegebenen Namen.
Prop illustriert den Umgang mit einer Enumeration als Beispiel für ein interface.
import java.util.*;
// Enumeration
/** A class to emulate the `printenv’ command */
class Prop {
/** displays some or all properties */
public static void main (String args []) {
if (args.length == 0) {
Enumeration e = System.getProperties().propertyNames();
while (e.hasMoreElements()) {
String key = (String)e.nextElement();
System.out.println(key +"\t"+ System.getProperty(key));
}
} else
for (int n = 0; n < args.length; ++ n) {
String p = System.getProperty(args[n]);
if (p != null)
System.out.println(p);
}
}
}
ist ein Interface, bei dem mit den Methoden hasMoreElements() und
nextElement() eine Sammlung durchlaufen werden kann.
Enumeration
2-19
liefert zwar ein Object, aber das kann wieder in seine ursprüngliche
Klasse umgewandelt und entsprechend verwendet werden — die Zulässigkeit der
Umwandlung wird zur Laufzeit geprüft.
nextElement()
liefert eine Enumeration für die SystemProperties. System.getProperty() liefert einen W ert zu einem Namen oder null.
Bestimmte Properties sollten existieren — sie sind aber in der Linux-V ersion zum Teil
falsch definiert.
System.getProperties().propertyNames()
Hier sind (sortiert und editiert) mögliche W erte von Windows 95:
awt.toolkit
file.separator
java.class.path
java.class.version
java.home
java.vendor.url
java.vendor
java.version
line.separator
os.arch
os.name
os.version
path.separator
user.dir
user.home
user.name
sun.awt.win32.MToolkit
\
.;d:\vorl\java\code;c:\java\lib;C:\JAVA\BIN\..\classes;
C:\JAVA\BIN\..\lib\classes.zip
45.3
C:\JAVA\BIN\..
http://www.sun.com/
Sun Microsystems Inc.
1.0
x86
Windows 95
4.0
;
D:\vorl\java\code\programs\char
C:\JAVA\BIN\..
axel
2-20
Ein Paket für Unicode
Ein Paket für Unicode
In diesem Abschnitt geht es um die Klassen UTFInputStream und UTFOutputStream zur
Umwandlung zwischen UTF und Unicode, die als Paket implementiert wurden.
wurde schon in char/Wc verwendet. char/Cvtdemonstriert, daß man
mit diesen Klassen sehr leicht Textdateien lokal anpassen kann.
UTFInputStream
Unicode und UTF
Unicode verwendet 16 Bit, um möglichst alle international existierenden Zeichen
darzustellen. Der Bereich 0..127 entspricht dabei dem ASCII-Zeichensatz.
UTF stellt Unicode durch ein bis drei Bytes je Unicode-Zeichen so dar, daß eine ASCIIDatei auch eine UTF-Datei ist:
.. \u007f
\u0080 .. \u07ff
\u0800 .. \uffff
\u0000
ein Byte
zwei Bytes
drei Bytes
0xxx xxxx
110x xxxx 10xx xxxx
1110 xxxx 10xx xxxx 10xx xxxx
Pakete
Ein Paket besteht aus mehreren Quellen und ist eine Zusammenfassung von
kooperierenden Klassen.
Die Klassen UTFInputStream und UTFOutputStream bilden zusammen das Paket
lib.utf. Dies ist hier nur vom Thema her begründet — Pakete haben jedoch die
Möglichkeit zum friendlyZugriff in den Klassen untereinander.
In jeder Quelle eines Pakets steht am Anfang die Anweisung package mit dem
Paketnamen. Der Paketname sollte sich von der DNS-Domain des Herstellers ableiten,
damit er weltweit eindeutig ist — wir sollten eigentlich de.uni-osnabrueck.informatik
als Vornamen verwenden.
Die Quellen, beziehungsweise bei Auslieferung die übersetzten Bytecode-Dateien,
befinden sich in einem Katalog, dessen Pfad aus den Komponenten des Paketnamens
gebildet wird und der vom CLASSPATH aus erreichbar ist.
Jede Quelle kann nur eine Klasse enthalten, die als public markiert ist. Sie legt den
Namen der Quelle fest.
2-21
UTF lesen — lib/utf/UTFInputStream
ist eine Unterklasse von FilterInputStream, die auf einen InputStream
aufgesetzt werden kann.
UTFInputStream
package lib.utf;
import java.io.*;
/** A class to input in UTF format */
public class UTFInputStream extends FilterInputStream {
/** pushes this onto in */
public UTFInputStream (InputStream in) {
super(in);
}
schützt den InputStream in, von dem Bytes gelesen werden. Der
Konstruktor UTFInputStream() ist nur nötig, weil der Konstruktor FilterInputStream()
das Attribut protected hat — es kann Unterklassen von FilterInputStream geben,
aber keine Objekte.
FilterInputStream
/** getUTF reads UTF-coded character
* @exception EOFException at end of file
*/
public char getUTF () throws IOException {
int c0;
String info = "";
switch ((c0 = read()) >> 4) {
case 0: case 1: case 2: case 3:
// 0xxx xxxx
case 4: case 5: case 6: case 7:
return (char)c0;
case 12: case 13:
// 110x xxxx 10xx xxxx
int c1 = read();
if ((c1 & 0xc0) == 0x80)
return (char)((c0 & 0x1f) << 6 | c1 & 0x3f);
info = " "+ c1;
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
c1 = read();
if ((c1 & 0xc0) == 0x80) {
int c2 = read();
if ((c2 & 0xc0) == 0x80)
return (char)
((c0 & 0x0f) << 12 | (c1 & 0x3f) << 6 | c2 & 0x3f);
info = " "+ c1 +" "+ c2;
} else
info = " "+ c1;
break;
default:
if (c0 == -1)
// eof ?
throw new EOFException();
}
throw new UTFDataFormatException("unexpected "+ c0 + info);
}
liefert einen char-Wert mit 16 Bit. Hier wurden wenig Klammern verwendet,
um die Vorrangverhältnisse der Bit-Operationen zu illustrieren.
getUTF()
2-22
/** getc reads UTF-coded character
* @return \n for any of \n \r\n \r
* @exception EOFException at end of file
*/
public char getc () throws IOException {
char c0;
switch (c0 = getUTF()) {
case ’\r’:
int c1;
switch (c1 = read()) {
default:
if (! (in instanceof PushbackInputStream))
in = new PushbackInputStream(in);
((PushbackInputStream)in).unread(c1);
case -1:
case ’\n’:
break;
}
case ’\n’:
return ’\n’;
}
return c0;
}
// \r
// \r eof
// \r \n
// \n
liefert ebenfalls einen char-Wert mit 16 Bit, verwandelt aber verschiedene
Zeilentrenner einheitlich in \n. read() ist die von FilterInputStream ererbte Methode,
die dort explizit von in liest — deshalb kann ein PushbackInputStream eingeschaltet
werden.
getc()
/** gets reads UTF-coded line terminated by \n|\r|\r\n|eof
* @return String without \n etc. or null at eof
*/
public String gets () throws IOException {
StringBuffer s = new StringBuffer();
try {
char c0;
while ((c0 = getc()) != ’\n’)
s.append(c0);
return s.toString();
} catch(EOFException e) {
return s.length() > 0 ? s.toString() : null;
}
}
liefert einen String für eine Zeile und überspringt dabei die Zeilentrenner.
String-Objekte stellt man dadurch her, daß man die Zeichen in StringBuffer sammelt
und dann mit toString() umwandelt.
gets()
2-23
/** get reads UTF-coded line terminated by \n|\r|\r\n|eof
* @return String or null at eof
*/
public String get () throws IOException {
StringBuffer s = new StringBuffer();
try {
char c0;
loop: for (;;) switch (c0 = getUTF()) {
case ’\r’:
s.append(c0);
int c1;
switch (c1 = read()) {
default:
// \r
if (! (in instanceof PushbackInputStream))
in = new PushbackInputStream(in);
((PushbackInputStream)in).unread(c1);
case -1:
// \r eof
break loop;
case ’\n’:
// \r \n
break;
}
case ’\n’:
// \n
s.append(’\n’);
break loop;
default:
s.append(c0);
}
return s.toString();
} catch(EOFException e) {
return s.length() > 0 ? s.toString() : null;
}
}
}
liefert ebenfalls eine Zeile, aber noch mit den Zeilentrennern. Leider muß hier der
Algorithmus aus getc() dupliziert werden.
get()
2-24
UTF schreiben — lib/utf/UTFOutputStream
ist eine Unterklasse von FilterOutputStream, die auf einen
OutputStream aufgesetzt werden kann.
UTFOutputStream
package lib.utf;
import java.io.*;
/** A class to output in UTF format */
public class UTFOutputStream extends FilterOutputStream {
/** pushes this onto out */
public UTFOutputStream (OutputStream out) {
super(out);
}
Konstruktion und Schutz entsprechen UTFInputStream. Es wäre sinnvoll,
UTFOutputStream auf einen BufferedOutputStream zu setzen, damit die Ausgabe
effizienter erfolgt.
/** putUTF writes UTF-coded character
* @return number of bytes written
*/
public synchronized int putUTF (char c)
if (c < ’\u0080’) {
// 0xxx
write(c);
return 1;
} else if (c < ’\u0800’) { // 110x
write(c >> 6 & 0x1f | 0xc0);
write(c & 0x3f | 0x80);
return 2;
} else {
// 1110
write(c >> 12 & 0x0f | 0xe0);
write(c >> 6 & 0x03f | 0x80);
write(c & 0x3f | 0x80);
return 3;
}
}
throws IOException {
xxxx
xxxx 10xx xxxx
xxxx 10xx xxxx 10xx xxxx
untersucht, in welchem Bereich das Argument liegt, und gibt dann ein bis
drei Bytes aus. Hier sieht man, wie man mit \u Unicode-Werte in char-Konstanten
angeben kann.
putUTF()
als Attribut einer Methode bedeutet, daß die Methode exklusiven Zugriff
auf ihren Empfänger hat — sie kann von keinem anderen Thread unterbrochen werden.
Dies ist hier wichtig, um die Korrektheit der UTF-Byte-Folge zu garantieren.
synchronized
/** putc writes UTF-coded character, replaces \n by line.separator
* @return number of bytes written
*/
public int putc (char c) throws IOException {
return c != ’\n’ ? putUTF(c) :
print(System.getProperty("line.separator", "\n"));
}
codiert sein Argument, ersetzt aber \n durch den lokalen Zeilentrenner, der als
String durch print() ausgegeben wird.
putc()
2-25
/** println writes UTF-coded line with line.separator
* @return number of bytes written
*/
public synchronized int println (String s) throws IOException {
int result = 0;
for (int n = 0; n < s.length(); ++ n)
result += putc(s.charAt(n));
return result + putc(’\n’);
}
verwendet putc() zur Ausgabe seiner Zeichen und fügt nocht einen lokalen
Zeilentrenner hinzu.
println()
/** print writes UTF-coded string
* @return number of bytes written
*/
public synchronized int print (String s) throws IOException {
int result = 0;
for (int n = 0; n < s.length(); ++ n)
result += putUTF(s.charAt(n));
return result;
}
}
print()
gibt einen String codiert aus.
Im Gegensatz zu anderen Methoden der Java-Bibliothek liefern diese Methoden immer ,
wieviele Bytes sie ausgegeben haben.
2-26
Text impor tieren — char/Cvt
Cvt kopiert Textzeilen aus der Standard-Eingabe oder aus Dateien und trennt sie durch
den lokalen Zeilentrenner.
Die Kombination von UTFInputStream und UTFOutputStream führt dazu, daß nur die
Zeilentrenner normalisiert werden.
// BUG(win95) `java Cvt x > y’ implicitly replaces \r --> \r\n
import java.io.*;
import lib.utf.*;
/** A class to display text with the local line separator */
class Cvt {
/** copy stdin or each argument file */
public static void main (String args []) {
try {
if (args.length == 0)
copyLines(System.in);
else
for (int n = 0; n < args.length; ++ n)
copyLines(new FileInputStream(args[n]));
} catch(Exception e) {
System.err.println(e.getClass().getName() +": "+ e.getMessage());
System.exit(1);
}
}
/** standard output */
static UTFOutputStream out = new UTFOutputStream(System.out);
/** copy in to out by lines */
static void copyLines (InputStream in) throws IOException {
UTFInputStream u = new UTFInputStream(in);
String s;
while ((s = u.gets()) != null)
out.println(s);
out.flush();
}
}
System.out
ist gepuffert, also ist kein expliziter BufferedOutputStream nötig.
Das Beispiel zeigt, daß man Klassenvariablen durchaus dynamisch initialisieren kann.
Alternativ kann man einen Block nach dem Wort static angeben, der bei Initialisierung
der Klasse einmal ausgeführt wird.
2-27
Die Kommandozeile als InputStream
Die Kommandozeile als InputStream
In diesem Abschnitt geht es darum, die Argumente der Kommandozeile als
InputStream zu bearbeiten. Ein Programm kann dann zum Beispiel wahlweise
Eingabezeilen oder Kommandoargumente mit den gleichen Methoden verarbeiten.
Arithmetische Ausdrücke bewerten — char/Calc
Calc liest Zeilen mit Gleitkomma-Ausdrücken von der Standard-Eingabe und bewertet
sie.
Calc demonstriert den Umgang mit StreamTokenizer und zeigt, wie man mit
numerischen Problemen umgehen kann.
import java.io.*;
// find I/O classes
/** A class to evaluate floating point expressions */
public class Calc {
/** reads lines from stdin and evaluates them */
public static void main (String args []) {
StreamTokenizer s = new StreamTokenizer(System.in);
s.commentChar(’#’);
// ignore # to eol
do {
Float f = expr(s);
if (! f.isNaN()) System.out.println(f);
} while (s.ttype == s.TT_EOL);
}
wird auf einen InputStream aufgesetzt und gibt dann Symbole durch
Methodenaufruf und in einer Instanzvariablen ab. Das Verhalten kann durch Definition
verschiedener Zeichenklassen beeinflußt werden, aber es eignet sich wohl am besten
zur Zerlegung einer Art Programmiersprache. StreamTokenizer liest Bytes, nicht UTF.
StreamTokenizer
wird hier als Resultat verwendet, denn nur dann kann man NaN erkennen und
bei der Ausgabe vermeiden.
Float
/** expr: sum \n */
public static Float expr (StreamTokenizer s) {
s.eolIsSignificant(true);
try {
lineloop: for (;;)
switch (s.nextToken()) {
default:
Float result = new Float(sum(s));
if (s.ttype != s.TT_EOL)
throw new Exception("expecting nl");
return result;
case s.TT_EOL: continue;
case s.TT_EOF: break lineloop;
}
} catch(Exception e) {
System.err.println(e.getClass().getName()+": "+e.getMessage());
}
return new Float(Float.NaN);
}
expr()
verarbeitet eine Zeile und verkapselt IOException. Die restlichen Funktionen
2-28
arbeiten mit float als Resultat, aber aus expr() muß auch bei einem Fehler noch ein
als defekt erkennbares Resultat geliefert werden. Da Java keine Parameter ändern
kann, muß man einen geeigneten Resultattyp einführen.
/** sum: product { +- product } */
public static float sum (StreamTokenizer s) throws IOException {
s.ordinaryChar(’-’); s.ordinaryChar(’/’);
float result = product(s);
for (;;)
switch (s.ttype) {
case ’+’:
s.nextToken(); result += product(s); continue;
case ’-’:
s.nextToken(); result -= product(s); continue;
default:
return result;
}
}
/** product: factor { *%/ factor } */
static float product (StreamTokenizer s) throws IOException {
float result = factor(s);
for (;;)
switch (s.ttype) {
case ’*’:
s.nextToken(); result *= factor(s); continue;
case ’/’:
s.nextToken(); result /= factor(s); continue;
case ’%’:
s.nextToken(); result %= factor(s); continue;
default:
return result;
}
}
und product() arbeiten mit rekursivem Abstieg. sum() ist public, damit man
nicht unbedingt über die Zeilenschnittstelle in expr() arbeiten muß.
sum()
StreamTokenizer
verarbeitet manche Zeichen etwas merkwürdig, aber man kann das
kontrollieren.
/** factor: +factor | -factor | (sum) | number */
static float factor (StreamTokenizer s) throws IOException {
switch (s.ttype) { float result;
case ’+’:
s.nextToken(); return factor(s);
case ’-’:
s.nextToken(); return - factor(s);
case ’(’:
s.nextToken(); result = sum(s);
if (s.ttype != ’)’)
throw new IllegalArgumentException("expecting )");
s.nextToken(); return result;
case s.TT_NUMBER:
result = (float)s.nval; s.nextToken(); return result;
default:
throw new IllegalArgumentException("missing factor");
}
}
}
verwendet eine IllegalArgumentException bei Syntaxfehlern. Diese stammt
von RuntimeException ab und muß deshalb nicht explizit deklariert oder abgefangen
werden.
factor()
2-29
Arithmetische Ausdrücke auf der Kommandozeile — char/Expr
Expr erweitert Calc um die Fähigkeit, auch einen Ausdruck auf der Kommandozeile zu
bewerten.
Expr demonstriert, daß man mit einem StringBuffer die Kommandozeile in einen
String zurückverwandeln kann, aus dem dann mit einem StringBufferInputStream ein
InputStream wird, den man mit den Methoden von Calc bearbeiten kann.
import java.io.*;
// find I/O classes
/** A class to mimic part of the expr command */
class Expr extends Calc {
/** main program, evaluates one expression on command line
* or expression lines from standard input
*/
public static void main (String args []) {
if (args.length == 0)
Calc.main(args);
else {
// turn command line into InputStream
StringBuffer b = new StringBuffer(args[0]);
for (int n = 1; n < args.length; ++ n)
b.append(" ").append(args[n]);
b.append("\n");
Float f = expr(new StreamTokenizer(
new StringBufferInputStream(b.toString())));
if (! f.isNaN()) System.out.println(f);
}
}
}
Expr.java befindet sich im gleichen Katalog und anonymen Paket wie Calc.java, deshalb
muß nicht importiert werden.
Klassenmethoden werden vererbt, aber man kann sie auch über die Klasse aufrufen.
super kann man in einer Klassenmethode — ebenso wie this — nicht verwenden, da
die Klassenmethode anders als in Objective C keinen Empfänger hat.
Der einzige Effekt der Vererbung ist, daß man expr() direkt und nicht als Calc.expr()
aufrufen kann.
2-30
Ein Framework für die Kommandozeile
Ein Framework für die Kommandozeile
In UNIX hat sich ein gewisser Standard für die Gestaltung von Kommandozeilen
eingebürgert:
Optionen gehen Argumenten voraus und beginnen mit einem Minuszeichen,
dem Flaggen folgen.
Flaggen sind einzelne Buchstaben, die beliebig in Optionen zusammengefaßt
werden können..
Zu einer Flagge kann ein Wert angegeben sein, der der Flagge als Rest der
Option oder als nächstes Argument folgt.
Ein einzelnes Minuszeichen beendet die Optionen und gilt als Argument —
normalerweise als Verweis auf die Standard-Eingabe.
Eine Option aus zwei Minuszeichen beendet die Optionen, gilt aber nicht als
Argument.
Da sich sehr viele Kommandos an diesen Standard halten, bietet sich an, dessen
Implementierung in einer Klasse Main zu verkapseln: Main erhält die Argumente von
einem Klienten-Objekt und schickt Nachrichten über decodierte Flaggen und
Argumente zurück an den Klienten, der deshalb das Interface CommandLine
implementieren muß.
Damit nicht alle Methoden aus diesem Interface explizit implementiert werden
müssen, gibt es eine Klasse MainClient, die CommandLine implementiert, und von der
ein Klient abgeleitet werden kann, der nur einige Methoden ersetzt.
MainClient erlaubt keine Flaggen und interpretiert die Argumente als Dateinamen, aus
denen ein fortlaufender InputStream konstruiert wird. Dies ist für manche
Filterprogramme recht praktisch.
In diesem Abschnitt wird die Implementierung des Frameworks besprochen sowie ein
triviales Beispiel main/Cmd. Realistischere Anwendungen sind eine Implementierung
des UNIX-Kommandos cat, eine Variante von char/Wc mit verschiedenen AusgabeOptionen und eine einfache Version des UNIX-Kommandos ls zur Anzeige von
Katalogen.
2-31
Das Interface — lib/main/CommandLine
definiert die Methoden, die ein Klient des Frameworks implementieren
muß, denn sie werden in verschiedenen Situationen aufgerufen.
CommandLine
package lib.main;
import java.io.*;
/** mandatory methods for client programs of Main */
public interface CommandLine {
/** called for each flag argument */
void flag (char ch, Main main);
/** called if there is no argument except, possibly, flags
* @return code for exit()
*/
int arg (Main main);
/** called for each argument
* @return code for exit(); non-0 aborts argument processing
*/
int arg (String arg, Main main) throws FileNotFoundException;
/** called after all arguments or as soon as arg() returns nonzero
* @return result for main.run()
*/
int exit (int code, Main main);
}
Wie bei class wird auch bei interface implizit extends Object angenommen; andere
Positionen in der Klassenhierarchie sowie eine implements-Klausel sind erlaubt.
Man muß sich Zugriffsschutz und Ausnahmen hier offenbar sehr genau überlegen:
Das interface selbst kann nur public erklärt werden.
Die Methoden können nicht geschützt werden und sie übernehmen ein publicAttribut von interface.
Die throws-Klauseln können später nicht erweitert werden.
2-32
Die Basisklasse — lib/main/MainClient
ist eine primitive Implementierung für CommandLine: Flaggen sind nicht
erlaubt; Argumente werden als Dateinamen interpretiert und als durchgehender
InputStream über in zur Verfügung gestellt — das hat den Nebeneffekt, daß geprüft
wird, ob Lesezugriff auf alle Argumente möglich ist.
MainClient
package lib.main;
import java.io.*;
/** A possible base class for client programs of Main */
public class MainClient implements CommandLine {
public void flag (char ch, Main m) {
throw new IllegalArgumentException("flag -"+ch+" not permitted");
}
/** concatenated from each argument, or System.in */
protected InputStream in;
public int arg (Main m) {
in = System.in;
return 0;
}
public int arg (String arg, Main m) throws FileNotFoundException {
if (arg.length() == 0) throw new FileNotFoundException("no filename");
InputStream next =
"-".equals(arg) ? System.in : new FileInputStream(arg);
in = in == null ? next : new SequenceInputStream(in, next);
return 0;
}
public int exit (int code, Main m) {
return code;
}
}
Mit einem SequenceInputStream kann man eine Folge von InputStream-Objekten
nacheinander in einem einzigen InputStream verwenden.
Die Lösung hat zwei Nachteile: Alle Dateiverbindungen sind letztlich gleichzeitig offen
und man verliert die Information über die Dateinamen, die zu den einzelnen
Verbindungen geführt haben — Fehlermeldungen sind später weniger spezifisch.
Ein leerer Dateiname sollte zwar nicht vorkommen, aber in einem Framework kann
man auch diesen Fehler zentral vermeiden.
2-33
Das Framework — lib/main/Main
Main
erzeugt Nachrichten für Optionen und Argumente einer UNIX-Kommandozeile.
package lib.main;
import java.io.*;
/** A framework to deal with a UNIX-style command line */
public class Main {
/** receives method calls for decoded arguments */
protected CommandLine client;
/** client’s class’ name for messages */
public String name;
/** constructor remembers client */
public Main (CommandLine client) {
this.client = client;
name = client.getClass().getName();
}
Bei der Konstruktion eines Main-Objekts wird ein Klienten-Objekt angegeben, das
CommandLine implementieren muß damit ihm alle Nachrichten über die Optionen und
Argumente gesandt werden können.
/** argument list to be decoded */
protected String args [];
/** current element in args[], next charAt() in args[a] */
protected int a, c;
/** main loop, calls CommandLine methods */
public int run (String _args []) throws IOException {
args = _args;
// flags loop
for (a = 0; a < args.length && args[a].length() > 0
&& args[a].charAt(0) == ’-’; ++ a) {
if (args[a].length() == 1) break;
// if (args[a].equals("--")) { ++ a; break; } // -for (c = 2; c <= args[a].length(); ++ c)
// -f
client.flag(args[a].charAt(c-1), this);
}
int result;
if (a < args.length)
do {
// arguments loop
c = args[a].length();
// so that arg() must advance
result = client.arg(args[a], this);
} while (result == 0 && ++a < args.length);
else
// no argument
result = client.arg(this);
result = client.exit(result, this);
if (System.out.checkError())
throw new IOException("output error");
return result;
}
implementiert vor allem die Methode run(), mit der eine Kommandozeile als
Vektor von Strings analysiert wird.
Main
bezeichnet das aktuelle Argument, c die Position nach der aktuellen Flagge. Der
Klient kann mit der folgenden Methode arg() zu einer Flagge einen Wert abholen:
a
2-34
/** option value specified after a flag, or next argument
* @return next value on each call
* @exception ArrayIndexOutOfBoundsException if there is none
*/
public String arg () {
String result =
c < args[a].length() ? args[a].substring(c) : args[++a];
c = args[a].length();
// so that arg() or run() must advance
return result;
}
Bei jedem Aufruf wird der nächste W ert geliefert. Da Java sowohl Vektor- als auch
String-Indizes kontrolliert, liefert arg() eine Exception, wenn unerlaubt angefragt wird.
bietet dem Klienten die Möglichkeit, abschließende Arbeiten durchzuführen —
ähnlich wie atexit() in ANSI-C.
exit()
/** standard error messages */
public void error (String msg) {
System.err.println(name +": "+ msg);
}
public void error (Exception e) {
String msg;
if (e == null)
error("exception");
else if ((msg = e.getMessage()) != null && msg.length() > 0)
error(e.getClass().getName() +": "+ msg);
else
error(e.getClass().getName());
}
/** standard fatal error messages, call client.exit() and quit */
public void fatal (String msg) {
if (msg == null)
error("fatal error");
else if (msg.length() > 0)
error(msg);
System.exit(client.exit(1, this));
}
public void fatal (Exception e) {
error(e);
fatal("");
}
public void fatal () {
fatal((String)null);
}
}
Damit Fehlermeldungen zentral ein einheitliches Format bekommen, definiert Main
dafür einige Methoden, die insbesondere auch zur Analyse einer Exception verwendet
werden können. Auf diese W eise kann exit() auch bei einem harten Fehler noch zum
Zug kommen.
, also Auswahl in Abhängigkeit von Name und Parameter-Typ,
Java erlaubt Overloading
für Methoden — allerdings gilt der Typ bei der Übersetzung. Wie man bei fatal()
sieht, kann eine explizite Umwandlung hier entscheidend sein.
2-35
Ein Testprogramm — main/Cmd
Cmd akzeptiert die Flagge f und die Flagge v mit einem W ert. Argumente müssen
Dateien sein, die gelesen werden können.
Cmd ist ein einfacher Test für CommandLine.
import lib.main.*;
/** A client class for a trivial command line program */
class Cmd extends MainClient {
public static void main (String args []) {
Main main = new Main(new Cmd());
try {
System.exit(main.run(args));
} catch(ArrayIndexOutOfBoundsException e) {
main.error("value for option missing");
} catch(Exception e) {
main.error(e);
}
System.err.println("usage: "+main.name+" [-f] [-v val] file...");
main.fatal();
}
public void flag (char ch, Main main) {
switch (ch) {
case ’f’:
System.out.println("-"+ch); break;
case ’v’:
System.out.println("-v "+main.arg()); break;
default:
super.flag(ch, main);
}
}
}
Cmd ist eine Unterklasse von MainClient. Beide Klassen definieren keine eigenen
Konstruktoren, erben also den parameterlosen Konstruktor von Object.
ist public, damit der Name der Klientenklasse leicht in Fehlermeldungen
verwendet werden kann.
main.name
ersetzt nur flag() und verweist dort alle unbekannten Flaggen an die Methode
flag() in MainClient, wo die Fehlerbehandlung ausgelöst wird.
Cmd
2-36
Dateien als Strom ausgeben
Dateien als Strom ausgeben — main/Cat
Cat kopiert seine Standard-Eingabe oder die als Argument angegebenen Dateien zur
Standard-Ausgabe. Die Ausgabe erfolgt gepuffert, falls dies nicht durch -u unterdrückt
wird.
Cat ist eine Implementierung des UNIX-Kommandos cat auf der Basis von MainClient.
import java.io.*;
import lib.main.*;
/** A client class to implement the ’cat’ command */
class Cat extends MainClient {
public static void main (String args []) {
Main main = new Main(new Cat());
try {
System.exit(main.run(args));
} catch(Exception e) {
main.error(e);
}
System.err.println("usage: "+main.name+" [-u] file...");
System.exit(1);
}
/** records -u to suppress output buffering */
private boolean unbuffered;
public void flag (char ch, Main main) {
switch (ch) {
case ’u’:
unbuffered = true; break;
default:
super.flag(ch, main);
}
}
/** does all the work -- copies in to System.out */
public int exit (int code, Main main) {
OutputStream out = System.out;
if (! unbuffered) out = new BufferedOutputStream(out);
byte buf [] = new byte [8192]; // improve input performance
int n;
try {
while ((n = in.read(buf)) != -1)
out.write(buf, 0, n);
out.flush();
} catch(IOException e) {
throw new RuntimeException("output error");
}
return 0;
}
}
2-37
Eine Klasse wiederverwenden
Eine Klasse wiederverwenden
Ein wesentliches Argument für den Einsatz objektorientierter Programmierung ist, daß
sich damit Code in Form von gut verkapselten Klassen leicht wiederverwenden lassen
soll. Leider hat der Weg zum Glück auch so seine Sackgassen..
Zeichen, Worte oder Zeilen zählen — main/UWc
UWc zählt Zeilen, Worte oder Zeichen in der Standard-Eingabe oder in den als
Argumente angegebenen Dateien. Mit den Schaltern -c, -l und -w kann die Ausgabe
eingeschränkt werden, der Schalter -u sorgt für die Interpretation im UTF-Format.
UWc ist eine Implementierung des UNIX-Kommandos wc auf der Basis von
CommandLine, wobei die Zählalgorithmen aus char/Wc wiederverwendet werden.
Die Lösung ist elegant, da ‘‘zufällig’’ ein geeigneter Konstruktor für Wc existiert. main/Ls
zeigt, wie man vorgehen kann, wenn dies nicht der Fall ist.
import java.io.*;
import lib.main.*;
import lib.utf.*;
/** A client class to implement the UNIX ’wc’ command */
class UWc extends Wc implements CommandLine {
public static void main (String args []) {
Main main = new Main(new UWc());
try {
System.exit(main.run(args));
} catch(Exception e) {
main.error(e);
}
System.err.println("usage: "+main.name+" [-cluw] file...");
System.exit(1);
}
Zur Wiederverwendung der Algorithmen erzeugt man eine Unterklasse. Zur Benutzung
des Frameworks muß dann das CommandLine-Interface implementiert werden.
Ein UWc-Objekt ist Klient von Main. Dieses Objekt benötigt keine Argumente zur
Konstruktion. Da Wc Konstruktoren hat, muß ein parameterloser Konstruktor für UWc
explizit definiert werden:
2-38
/** this is only for the main object */
private UWc () {
}
/** record flags -- across all objects */
private static boolean any, chars, lines, words, runes;
public void flag (char ch, Main main) {
switch (ch) {
case ’c’:
any = chars = true; break;
case ’l’:
any = lines = true; break;
case ’u’:
runes = true; break;
case ’w’:
any = words = true; break;
default:
main.fatal("flag -"+ch+" not permitted");
}
}
Der Klient notiert dann die Flaggen. Da sie sich auf alle Ausgaben (alsoWc-Objekte)
auswirken sollen, werden sie in Klassenvariablen notiert.
Da UWc nicht von MainClient abstammt, kann super.flag() nicht aufgerufen werden.
ist private. Trotzdem gibt es UWc-Objekte, denn zum einen gibt es noch andere
Konstruktoren, und zum andern kann der Konstruktor in einer Klassenmethode wie
main() innerhalb von UWc natürlich aufgerufen werden.
UWc()
muß, wie jeder Unterklassen-Konstruktor, einen Oberklassen-Konstruktor explizit
oder implizit aufrufen. Wc hat (bisher...) nur Konstruktoren, die Zählalgorithmen
implementieren. Der folgende Konstruktor , der noch in Wc definiert sein muß, umgeht
das Problem. Anders als in Objective C kann in Java eine Klasse nicht nachträglich
erweitert werden.
UWc()
/** dummy constructor for command line version */
protected Wc () {
}
Das Ziel der Implementierung ist es, die Konstruktoren von Wc zu übernehmen, aber
toString() so zu ersetzen, daß die Flaggen wirksam werden:
/** overwrite some methods of Wc */
UWc (InputStream in) throws IOException {
super(in);
}
UWc (UTFInputStream in) throws IOException {
super(in);
}
public String toString () {
if (! any)
return super.toString();
StringBuffer s = new StringBuffer();
String sep = "";
if (lines) { s.append(l); sep = "\t"; }
if (words) { s.append(sep + w); sep = "\t"; }
if (chars) s.append(sep + c);
return s.toString();
}
}
Konstruktoren werden nicht vererbt sondern müssen so wie hier bereitgestellt werden.
2-39
/** no arguments -- process System.in */
public int arg (Main main) {
wc(System.in, "", main);
return 0;
}
/** argument -- process file */
public int arg (String arg, Main main) throws FileNotFoundException {
if ("-".equals(arg))
wc(System.in, "", main);
else if (arg.length() == 0)
throw new FileNotFoundException("no filename");
else
wc(new FileInputStream(arg), "\t"+arg, main);
return 0;
}
Die verschiedenen Argumente werden an eine zentrale Methode weitergeleitet:
/** process
private
private
try
and record number of files processed */
int files;
void wc (InputStream in, String trailer, Main main) {
{
UWc f = runes ? new UWc(new UTFInputStream(in)) : new UWc(in);
System.out.println(f + trailer);
++ files; l += f.l; w += f.w; c += f.c;
} catch(IOException e) {
main.error(e);
}
}
Der Klient ist ein UWc- und damit ein Wc-Objekt, hat also Instanzvariablen zum Zählen.
Sie werden dazu verwendet, die Summe über alle Dateien zu notieren.
Innerhalb von Methoden hat man auch Zugriff auf die Instanzvariablen von fremden
Objekten der eigenen Klasse.
/** produce summary */
public int exit (int code, Main main) {
if (files > 1)
System.out.println(this + "\ttotal");
return 0;
}
Damit gibt es eine besonders elegante Implementierung für die Ausgabe der Zeile mit
den Summen: Man muß nur toString() für den Klienten auslösen.
2-40
Information über Dateien
Information über Dateien — main/Ls
Ls informiert über den aktuellen Katalog oder über die Pfade, die als Argumente
angegeben sind. Mit der Flagge -l wird möglichst viel Information ausgegeben. Mit der
Flagge -d wird Information über einen Katalog statt über seinen Inhalt ausgegeben.
Nachrangig dazu wird mit der Flagge -R ein Katalog rekursiv traversiert.
Ls ist eine einfache Version des UNIX-Kommandos ls und illustriert einige der
Möglichkeiten, die die Klasse File zur Umgang mit Dateien bietet.
Da File keinen parameterlosen Konstruktor besitzt, muß man, anders als im Fall von
Wc, separate Klassen für Main-Klient und Informationsalgorithmus einführen. LsFile
dient zum Ersatz von toString() unter Kontrolle einer Flagge:
/** A class to produce information about a path */
class LsFile extends File {
/** if set, produce more information */
public boolean lflag;
/** create a path description */
public LsFile (String fnm, boolean lflag) {
super(fnm);
this.lflag = lflag;
}
/** format the information */
public String toString () {
StringBuffer s = new StringBuffer();
if (lflag) {
if (isDirectory()) s.append("d");
else if (isFile()) s.append("-");
else
s.append("?");
s.append(canRead() ? "r" : "-")
.append(canWrite() ? "w" : "-")
.append("\t" + length() + "\t");
}
return s.append(getPath()).toString();
}
}
ist deutlich so konzipiert, daß nur portable Informationen verfügbar sind.
Insbesondere kann man auch symbolische Links nicht erkennen, was Ls gefährlich
macht.
File
Für Ls gibt es keine Randbedingungen, folglich kann die Klasse von MainClient
abstammen. Wie üblich werden die Flaggen notiert — da es nur ein Klienten-Objekt
gibt, genügen Instanzvariablen.
2-41
import java.io.*;
import lib.main.*;
/** A client class to implement a simple ’ls’ command */
class Ls extends MainClient {
public static void main (String args []) {
Main main = new Main(new Ls());
try {
System.exit(main.run(args));
} catch(Exception e) {
main.error(e);
}
System.err.println("usage: "+main.name+" [-dlR] path...");
System.exit(1);
}
/** record flags */
boolean dflag, lflag, Rflag;
public void flag (char ch, Main main) {
switch (ch) {
case ’d’:
dflag = true; return;
case ’l’:
lflag = true; return;
case ’R’:
Rflag = true; return;
default:
super.flag(ch, main);
}
}
Die Argumente kann man wieder an eine zentrale Funktion weiterleiten:
/** no argument -- process current directory */
public int arg (Main main) {
ls("", System.getProperties().getProperty("user.dir", "."));
return 0;
}
/** argument -- process a path */
public int arg (String arg, Main main) {
ls(".".equals(arg) ? "" : arg+File.separator, arg);
return 0;
}
Man kann eigentlich nicht davon ausgehen, daß der Name . überall auf den aktuellen
Katalog verweist. Mit der System-Property user.dir kann man den aktuellen Katalog
feststellen.
Zur Verschönerung der Ausgabe übergibt man den Katalog-Präfix separat vom
kompletten Pfad.
2-42
/** process */
private void ls (String dirname, String path) {
LsFile f = new LsFile(path, lflag);
if (! dflag && f.isDirectory()) {
String names [] = f.list();
LsFile files [] = new LsFile [names.length];
for (int n = 0; n < names.length; ++ n)
files[n] = new LsFile(dirname + names[n], lflag);
for (int n = 0; n < files.length; ++ n)
System.out.println(files[n]);
if (Rflag)
for (int n = 0; n < files.length; ++ n)
if (files[n].isDirectory())
ls(dirname+names[n]+File.separator,
files[n].getPath());
} else
System.out.println(f);
}
}
hilft dadurch bei der Traverse eines Katalogs, daß die einfachen Dateinamen (ohne
. und ..) mit der Methode list() als Vektor geliefert werden.
File
erzeugt auch Vektoren von primitiven Null-W erten oder Null_Objekt-Verweisen. Ein
derartiger Vektor muß dann immer noch gefüllt werden.
new
ist eine Klassenvariable, mit der der lokale Komponententrenner
portabel gehalten wird. Es gibt ihn auch als System-Property. Die Lösung geht von
homogenen Pfaden aus, die zum Beispiel bei Windows nicht vorliegen.
File.separator
Leider ist die Windows-Implementierung von File reichlich defekt in bezug auf die
Wurzel eines Laufwerks:
c> java Ls c:
c:
c> java Ls c:\
Ls: java.lang.NullPointerException
usage: Ls [-dlR] path...
c> java Ls \
Ls: java.lang.NullPointerException
usage: Ls [-dlR] path...
C> java Ls c:\.
Nur der letzte Versuch funktioniert. Man kann zwar das vorliegende Beispiel reparieren,
aber eigentlich sollte man auf Korrektur der Klasse drängen...
2-43
Einen Algorithmus verkapseln
Einen Algorithmus verkapseln
Bei den zu Java gehörigen Packages befindet sich keine Sortiermethode.
In diesem Abschnitt wird zuerst ein naives Sortierprogramm implementiert.
Anschließend wird eine Methodik entwickelt, mit der verschiedene Sortieralgorithmen
so verkapselt werden können, daß sie sich auf verschiedene Object-Sammlungen mit
verschiedenen Vergleichskriterien anwenden lassen. Daraus kann dann ein modulares
Sortierprogramm aufgebaut werden.
Die Quellen befinden sich in zwei Katalogen: programs/sortenthält eine Reihe von
Testprogrammen, lib/sortenthält das Paket, das die Sortierung verkapselt. Das Paket
besteht aus öffentlichen und lokalen Klassen und versucht, deren Sichtbarkeit sinnvoll
zu begrenzen — darauf wird im Folgenden nicht explizit eingegangen.
Es zeigt sich, daß verschiedene Entscheidungen innerhalb der zu Java gehörigen
Packages nicht unbedingt hilfreich sind.
Ein naives Sortierprogramm — sor t/FirstSor t
FirstSortsortiert die Zeilen seiner Standard-Eingabe oder der als Argumente
angegebenen Dateien mit dem Sortierverfahren von Shell und gibt das Resultat als
Standard-Ausgabe aus.
FirstSortist eine sehr naive Implementierung des UNIX-Kommandos sort auf der Basis
von MainClient.
2-44
import java.io.*;
import java.util.*;
import lib.main.*;
/** A client class to naively implement a ’sort’ command */
class FirstSort extends MainClient {
public static void main (String args []) {
Main main = new Main(new FirstSort());
try {
System.exit(main.run(args));
} catch(Exception e) {
main.error(e);
}
System.err.println("usage: "+main.name+" file...");
System.exit(1);
}
/** does all the work */
public int exit (int code, Main main) {
Vector v = new Vector();
DataInputStream in = new DataInputStream(this.in);
try {
String line;
while ((line = in.readLine()) != null)
v.addElement(line);
} catch(IOException e) {
throw new RuntimeException("input error:"+e.getMessage());
}
shellsort(v);
for (Enumeration e = v.elements(); e.hasMoreElements(); )
System.out.println(e.nextElement());
return code;
}
/** Shell sort, based on K&R */
private void shellsort (Vector v) {
for (int gap = v.size()/2; gap > 0; gap /= 2)
for (int i = gap; i < v.size(); ++ i)
for (int j = i-gap; j >= 0; j -= gap) {
String a = (String)v.elementAt(j);
String b = (String)v.elementAt(j+gap);
if (a.compareTo(b) > 0) {
v.setElementAt(b, j);
v.setElementAt(a, j+gap);
} else
break;
}
}
}
Zeilen werden mit readLine() von DataInputStream extrahiert und als Elemente eines
Vector-Objekts gespeichert, dessen Kapazität automatisch wächst.
shellsort()
sortiert den Vector relativ zu compareTo() aus String.
stellt mit elements() eine Enumeration seiner Elemente bereit, die dabei
wirklich vom kleinsten zum größten Index durchlaufen werden. Damit kann der
sortierte Vector ausgegeben werden.
Vector
beruht auf dem Zugriff zu einzelnen Elementen im Vector und auf der
Verfügbarkeit von compareTo() — wozu aber Kenntnis der Element-Klasse nötig ist.
shellsort()
2-45
Design zur Wiederverwendung — lib/sor t
FirstSorthat den entscheidenden Nachteil, daß shellsort() praktisch nicht
wiederverwendet werden kann, obgleich ja wenigstens das Sortierobjekt einigermaßen
allgemein spezifiziert ist.
Man könnte versuchen, eine Unterklasse von Vector mit einem Sortieralgorithmus zu
konstruieren, aber es geht mit noch geringeren Voraussetzungen.
dient dazu, eine Sammlung als einmal verwendbare Folge abzubilden. Das
folgende Programm A zeigt, daß man damit relativ leicht auch die Argumente der
Kommandozeile ausgeben kann:
Enumeration
import lib.sort.*;
class A {
public static void main (String args []) {
new Print(new Elements(args));
}
}
ist eine lokale Klasse, deren Konstruktor dazu dient, eine Enumeration zur
Standard-Ausgabe zu schicken — eine ineffiziente Alternative zu einer
Klassenmethode:
Print
import java.util.*;
/** A local class to print (and consume) an enumeration */
class Print {
public Print (Enumeration e) {
while (e.hasMoreElements())
System.out.println(e.nextElement());
}
}
ist eine der Klassen, aus denen das Sortiersystem aufgebaut wird. Mit
Elements kann man einen Object-Vektor als Enumeration abbilden — über copyInto()
kann ein solcher Vektor zwar auch als Vector und von dort mit elements() als
Enumeration zugänglich gemacht werden, aber das ist unnötig aufwendig.
Elements
2-46
package lib.sort;
import java.io.*;
import java.util.*;
/** A class to enumerate the elements in a Object [] */
public class Elements implements Enumeration {
private Object vector [];
// to get elements from
private int next;
// lookahead
/** constructor remembers vector */
public Elements (Object vector []) {
this.vector = vector;
next = 0;
}
/** implement Enumeration of elements in vector */
public boolean hasMoreElements () {
return next < vector.length;
}
public Object nextElement () {
if (hasMoreElements()) {
++ next;
return vector[next-1];
}
throw new NoSuchElementException(vector.toString());
}
}
Die entscheidende Idee besteht jetzt darin, Klassen zu konstruieren, die Werte und
Sortierschlüssel verkapseln, dann eine Enumeration so zu maskieren, daß sie als Folge
von Schlüsseln erscheint, und schließlich einen Sortieralgorithmus auf der Enumeration
von Schlüsseln zu definieren.
2-47
ist eine abstrakte Klasse, die Object-Werte mit Sortierschlüsseln kapselt,
Vergleiche ermöglicht und toString() auf den ursprünglichen Wert bezieht:
Comparable
package lib.sort;
import java.util.*;
/** A local abstract base class to compare keys and contain values */
abstract class Comparable {
protected Object value;
Comparable (Object value) {
this.value = value;
}
/** compare key part and return < 0, == 0, or > 0 */
abstract int compareTo (Comparable another);
/** return value part as String */
public String toString () {
return value.toString();
}
}
Eine Klasse heißt abstrakt, wenn nicht alle Methoden implementiert sind. Eine
Methode wird dann mit abstract markiert und hat keinen Körper. Ein Interface
besteht nur aus abstrakten Methoden, wobei das Attribut abstract entfallen kann, und
hat keinen Konstruktor.
Eigentlich kapselt Comparable nur einen Aspekt, den String bereits implementiert —
das läßt sich aber nicht mehr nachbessern.
Da String außerdem als final definiert ist, kann man keine Unterklasse erzeugen —
das erlaubt zwar dem Compiler, besseren Code zum Methodenaufruf zu erzeugen,
denn als final markierte Methoden und Methoden in einer als final markierten Klasse
können nicht mehr ersetzt werden, aber es macht CompareAsString unnötig
kompliziert:
package lib.sort;
import java.util.*;
/** A local class to make a String a Comparable (yuck!) */
class CompareAsString extends Comparable {
CompareAsString (String value) {
super(value);
}
int compareTo (Comparable another) {
return ((String)value).compareTo((String)another.value);
}
}
Jetzt kann man beispielsweise eine Enumeration von String-Elementen so maskieren,
daß sie direkt als String-Objekte verglichen werden. Dazu muß nextElement() ersetzt
werden, denn dort erfolgt die Maskerade.
2-48
Wenn man eine Methode ersetzen will, muß man eine Klasse ableiten. Ein Interface
kann man zwar ableiten, aber man kann dabei keine Methode implementieren sondern
nur Methoden zusätzlich deklarieren.
ist eine abstrakte Basisklasse, mit der Enumeration so implementiert wird, daß man
nextElement() ersetzen kann:
As
package lib.sort;
import java.util.*;
/** A local abstract base class to mask Enumeration */
abstract class As implements Enumeration {
protected Enumeration e;
public As (Enumeration e) {
this.e = e;
}
public boolean hasMoreElements () {
return e.hasMoreElements();
}
abstract public Object nextElement ();
}
AsStrings
maskiert eine Enumeration von String-Objekten mit CompareAsString:
package lib.sort;
import java.util.*;
/** A class to mask Enumeration of String as CompareAsString */
public class AsStrings extends As {
public AsStrings (Enumeration e) {
super(e);
}
public Object nextElement () {
return new CompareAsString((String)e.nextElement());
}
}
Außer Arbeitsvorbereitung ist noch nichts Nützliches passiert. Das Programm B gibt
nach wie vor nur die Argumente der Kommandozeile aus:
import lib.sort.*;
class B {
public static void main (String args []) {
new Print(new AsStrings(new Elements(args)));
}
}
2-49
Ein verkapseltes Sortierverfahren
Jetzt kann man aber ein Sortierverfahren auf einer Enumeration von Comparable
aufsetzen und so wiederverwendbar kapseln. Das Programm C liefert die
Kommandozeile sortiert:
import lib.sort.*;
class C {
public static void main (String args []) {
new Print(new InsertionSort(
new AsStrings(new Elements(args))).elements());
}
}
Da eine Enumeration beim Durchgang konsumiert wird, muß man zum Sortieren
zwischenspeichern. Zur Implementierung bietet sich ein Vector an — der Index-Vektor
einer konventionelleren Implementierung von Sortierverfahren. Dieser InsertionSort
speichert außerdem das Resultat der Sortierung für mehrfache Abfrage.
package lib.sort;
import java.util.*;
/** A class to sort an Enumeration of Comparable by insertion */
public class InsertionSort extends Vector {
/** inserts Enumeration into this, stable sorted up (or down) */
public void insertSorted (Enumeration e, boolean up) {
while (e.hasMoreElements()) {
Comparable c = (Comparable)e.nextElement();
int n = elementCount;
while (n -- > 0)
if (up) {
if (c.compareTo((Comparable)elementData[n]) >= 0)
break;
} else if (c.compareTo((Comparable)elementData[n]) <= 0)
break;
insertElementAt(c, n+1);
}
}
/** constructor, inserts Enumeration */
public InsertionSort (Enumeration e, boolean up) {
insertSorted(e, up);
}
/** constructor, inserts Enumeration sorted up */
public InsertionSort (Enumeration e) {
this(e, true);
}
}
stützt sich nur noch auf eine Enumeration von Comparable-Objekten. Je
nach Konstruktor werden die Objekte so (stabil) notiert, daß sie aufwärts oder abwärts
vom Anfang des Vector-Objekts her als elements() sortiert erscheinen.
InsertionSort
Leider sind fast alle Vector-Methoden als final definiert — sonst hätte man
wesentlich eleganter ableiten und auch für zeilenweise Ausgabe sorgen können.
2-50
Andere Elementfolgen
Programm D gibt die Zeilen der Standard-Eingabe sortiert aus:
import lib.sort.*;
class D {
public static void main (String args []) {
try {
new Print(new InsertionSort(
new AsStrings(new InputLines(System.in))).elements());
} catch(Exception e) {
System.err.println(e.getClass().getName()+": "+e.getMessage());
}
}
}
Dazu müssen nur noch mit InputLines die Zeilen eines InputStream als Enumeration
von String-Objekten geliefert werden:
package lib.sort;
import java.io.*;
import java.util.*;
/** A class to enumerate the lines in an InputStream */
public class InputLines implements Enumeration {
private DataInputStream in;
// to get lines from
private String nextLine;
// one line lookahead, null at eof
/** constructor remembers input stream and reads ahead */
public InputLines (InputStream in) throws IOException {
this.in = in instanceof DataInputStream
? (DataInputStream)in
: new DataInputStream(in);
nextLine = this.in.readLine();
}
/** implement Enumeration of lines in InputStream */
public boolean hasMoreElements () {
return nextLine != null;
}
public Object nextElement () {
if (hasMoreElements())
try {
Object result = nextLine;
nextLine = in.readLine();
return result;
} catch(IOException e) {
throw new NoSuchElementException(e.getMessage());
}
throw new NoSuchElementException(in.toString());
}
}
Lästig ist hier, daß eine IOException aus der Enumeration heraus als Unterklasse von
RuntimeException geliefert werden muß.
2-51
Die Sortierung kann auf jede Enumeration angewendet werden. Programm E gibt die
System-Properties nach Schlüsseln sortiert aus:
import java.util.*;
import lib.sort.*;
class E {
public static void main (String args []) {
Enumeration n = System.getProperties().propertyNames();
n = new InsertionSort(new AsStrings(n)).elements();
while (n.hasMoreElements()) {
String s = n.nextElement().toString();
System.out.println(s+"\t"+System.getProperty(s));
}
}
}
Andere Schlüssel
Wenn man nach anderen Kriterien sortieren will, muß man nur die Art der Schlüssel
verändern. Programm F sortiert Gleitkomma-W erte:
import lib.sort.*;
class F {
public static void main (String args []) {
try {
new Print(new InsertionSort(
new AsFloats(new InputLines(System.in))).elements());
} catch(Exception e) {
System.err.println(e.getClass().getName()+": "+e.getMessage());
}
}
}
Programm G sortiert absteigend:
import lib.sort.*;
class G {
public static void main (String args []) {
try {
new Print(new InsertionSort(
new AsFloats(new InputLines(System.in)), false).elements());
} catch(Exception e) {
System.err.println(e.getClass().getName()+": "+e.getMessage());
}
}
}
Dazu wird nur ein InsertionSort für die umgekehrte Reihenfolge konstruiert.
2-52
Beide Programme beruhen auf der Klasse CompareAsFloat, die float-Werte als
Schlüssel kapselt:
package lib.sort;
import java.util.*;
/** A local class to make a String a Comparable float value */
class CompareAsFloat extends Comparable {
private float key;
CompareAsFloat (String value) {
super(value);
try {
key = Float.valueOf(value).floatValue();
} catch(NumberFormatException e) {
}
}
int compareTo (Comparable another) {
float b = ((CompareAsFloat)another).key;
return key < b ? -1 : key > b ? 1 : 0;
}
}
maskiert dann eine Enumeration von String-Objekten, so daß
CompareAsFloat-Objekte zum Vergleich herangezogen werden:
AsFloats
package lib.sort;
import java.util.*;
/** A class to mask Enumeration of String as CompareAsFloat */
public class AsFloats extends As {
public AsFloats (Enumeration e) {
super(e);
}
public Object nextElement () {
return new CompareAsFloat((String)e.nextElement());
}
}
2-53
Das Sortierprogramm — sor t/Sor t
Sort sortiert Zeilen der Standard-Eingabe oder aus den als Argumenten angegebenen
Dateien in auf- oder (-r) absteigender Reihenfolge als Strings oder (-n) als GleitkommaWerte.
Sort kombiniert die verschiedenen Klassen des lib.sort-Pakets.
import
import
import
import
java.io.*;
java.util.*;
lib.main.*;
lib.sort.*;
/** A client class to implement a ’sort’ command */
class Sort extends MainClient {
public static void main (String args []) {
Main main = new Main(new Sort());
try {
System.exit(main.run(args));
} catch(Exception e) {
main.error(e);
}
System.err.println("usage: "+main.name+" [-nr] file...");
System.exit(1);
}
/** record flags */
private static boolean numeric;
// sort numerically
private static boolean reverse;
// sort in reverse order
public void flag (char ch, Main main) {
switch (ch) {
case ’n’:
numeric = true; break;
case ’r’:
reverse = true; break;
default:
main.fatal("flag -"+ch+" not permitted");
}
}
/** done -- display lines */
public int exit (int code, Main main) {
try {
Enumeration e = new InputLines(in);
e = numeric ? (Enumeration) new AsFloats(e)
: (Enumeration) new AsStrings(e);
new Print(new InsertionSort(e, !reverse).elements());
return code;
} catch(IOException e) {
throw new RuntimeException("IOException: "+e.getMessage());
}
}
}
2-54
Threads
Threads
Java Programme können aus quasi-parallel ablaufenden Threads bestehen. Dieser
Abschnitt zeigt an ein einfachen Beispielen, wie Threads erzeugt und manipuliert
werden, wie man den Zugriff auf gemeinsame V ariablen synchronisiert und dabei
Bedingungen abprüft und wie man Threads mit Pipelines verknüpft.
Many — thread/Many.java
Many erzeugt für jedes Argument einen Thread, der das Argument so oft ausgibt, wie
insgesamt Argumente auf der Kommandozeile angegeben sind. Jeder Thread gibt zum
Schluß einen Zeilentrenner aus.
Many zeigt, wie Threads erzeugt werden. Leider laufen die Threads sequentiell ab und
die Ausgabe deutet nicht auf parallele Ausführung hin.
/** A very simple thread demo */
class Many extends Thread {
/** each thread prints ’info’ exactly ’retry’ times */
int retry; String info;
Many (int retry, String info) {
this.retry = retry; this.info = info;
}
/** body of each thread */
public void run () {
for (int n = 0; n < retry; ++ n)
work();
quit();
}
/** can be overwritten... */
void work () {
System.out.print(info);
}
void quit () {
System.out.print(’\n’);
}
/** creates one thread per argument to retry for all arguments */
public static void main (String args []) {
for (int n = 0; n < args.length; ++ n)
new Many(args.length, args[n]).start();
}
}
Ein Thread führt die Methode run() parallel zu anderen Threads aus. Er entsteht
entweder als Unterklasse von Thread oder durch Implementierung von Runnable und
Übergabe eines entsprechenden Objekts bei Konstruktion eines Thread-Objekts.
beginnt mit der Ausführung, wenn das Thread-Objekt mit start() dazu
aufgefordert wird.
run()
2-55
Randy — thread/Randy.java
Randyerzeugt für jedes Argument einen Thread, der das Argument so oft ausgibt, wie
insgesamt Argumente auf der Kommandozeile angegeben sind. Der letzte Thread soll
zum Schluß einen Zeilentrenner ausgeben.
Randyerweitert Many und zeigt, wie die Priorität von Threads manipuliert werden
kann, so daß die Ausgabe der verschiedenen Threads vermischt wird. Das Kriterium für
die Ausgabe des Zeilentrenners ist absichtlich so programmiert, daß es häufig nicht
erfüllt wird.
import java.util.*; // Random
/** A thread demo with random behaviour */
class Randy extends Many {
/** count existing Randy objects */
static int nRandy;
Randy (int retry, String info) {
super(retry, info);
++ nRandy;
}
/** work at random priority */
static Random r = new Random();
void work() {
setPriority(MIN_PRIORITY +
(int)(r.nextDouble() * (MAX_PRIORITY - MIN_PRIORITY)));
super.work();
}
/** last thread writes single newline... */
void quit() {
int nr = nRandy;
// deliberate race condition
try {
sleep((int)(10 * r.nextDouble()));
} catch(InterruptedException e) {
}
if (nr == 1)
super.quit();
else
nRandy --;
}
public static void main (String args []) {
if (args.length > 0) {
Thread t [] = new Thread [args.length];
for (int n = 0; n < args.length; ++ n)
t[n] = new Randy(args.length, args[n]);
for (int n = 0; n < args.length; ++ n)
t[n].start();
}
}
}
Ein Random-Objekt ist die Quelle für eine Folge von Pseudo-Random-Werten, die mit
Methoden wie nextDouble() angefordert werden.
ändert die Priorität, mit der ein Thread ausgeführt wird. sleep()
suspendiert einen Thread für eine Anzahl von Millisekunden, kann jedoch durch die
(nicht implementierte) Methode interrupt() unterbrochen werden.
setPriority()
2-56
Morse — thread/Morse.java
Morse erweitert Randyso, daß der abschließende Zeilentrenner korrekt erzeugt wird.
Morse demonstriert, wie Threads den Zugriff auf globale Variablen synchronisieren
müssen.
/** A thread demo with a clean ending */
class Morse extends Randy {
Morse (int retry, String info) {
super(retry, info);
}
/** last thread really writes single newline */
void quit() {
synchronized(getClass()) {
super.quit();
}
}
public static synchronized void main (String args []) {
for (int n = 0; n < args.length; ++ n)
new Morse(args.length, args[n]).start();
}
}
Threads können exklusiven Zugriff zu einem Objekt bekommen, wenn sie in einer
synchronized-Anweisung oder durch Aufruf einer Methode mit Attribut synchronized
zugreifen. Klassenmethoden sperren dabei die Klassenbeschreibung, andere Methoden
sperren den Empfänger, die Anweisung sperrt den angegebenen W ert. Das Attribut
muß vor dem Resultattyp stehen.
2-57
Any — thread/Any.java
Any ist ein typisches consumer/producer
-Beispiel mit einem Produzenten, der die
Argumente der Kommandozeile (die nicht leer sein sollten) in einen globalen Puffer
schreibt, und vielen Konsumenten, die abwechselnd auf den Puffer zugreifen und die
Information ausgeben. Ab und zu schreibt der Produzent leere Strings, die zum
Abbruch der Konsumenten führen.
Any demonstriert conditional critical regions
, das heißt, wie man innerhalb von
synchronized eine Bedingung überprüft und bei Mißerfolg die Kontrolle wieder abgibt.
import java.util.*;
/** A consumer/producer demo */
class Any extends Thread {
static String info;
static Random r = new Random();
// common object
/** consumer tries to print info, terminates on empty */
public void run () {
synchronized(r) {
for (;;) {
while (info == null)
// acquire condition
try {
r.wait();
} catch(InterruptedException e) {
}
String copy = info; info = null;
r.notifyAll();
// release condition
if (copy.equals("")) stop();
// thread exits
setPriority(MIN_PRIORITY +
(int)(r.nextDouble() * (MAX_PRIORITY - MIN_PRIORITY)));
System.out.print(getName() + copy);
}
}
}
Any (String name) {
super(name);
}
/** producer places arguments (assumed nonempty) into info */
public static void main (String args []) {
for (int n = 0; n < args.length; ++ n)
new Any(n + "").start();
try {
synchronized(r) {
for (int n = 0; n < args.length; ++ n)
for (int m = 0; m <= args.length; ++ m) {
info = m == args.length ? "" : args[m];
r.notifyAll();
while (info != null) r.wait();
}
}
System.out.print(’\n’);
} catch(Exception e) {
System.err.println(e.getClass().getName()+": "+e.getMessage());
}
}
}
etabliert einen Monitor für einen kritischen Bereich. wait() suspendiert
den aufrufenden Thread, bis notify() oder notifyAll() aufgerufen wird; Ziel dieser
synchronized
2-58
Aufrufe ist das synchronized-Objekt. Wie vorgeführt, wird dann unter dem Schutz von
synchronized eine Bedingung überprüft und eventuell wieder wait() ausgeführt.
Ein einfacher Scanner
GrepAllkopiert die Zeilen von der Standard-Eingabe zur Standard-Ausgabe, die alle
Kommandoargumente in beliebiger Reihenfolge exakt enthalten.
Grep leistet dasselbe, allerdings nur für ein Kommandoargument.
Grep dient als Baustein in einer Pipeline von Such-Threads.GrepAllzeigt, wie Threads
mit PipedInputStream und PipedOutputStream kommunizieren können.
import java.io.*;
/** A class to scan a stream for lines with a text */
class Grep implements Runnable {
PrintStream out; String text; DataInputStream in;
public Grep (OutputStream out, String text, InputStream in) {
this.out = out instanceof PrintStream
? (PrintStream)out
: new PrintStream(out);
this.text = text;
this.in = in instanceof DataInputStream
? (DataInputStream)in
: new DataInputStream(in);
}
public void run () {
String line;
try {
while ((line = in.readLine()) != null)
if (line.indexOf(text) >= 0)
out.println(line);
} catch(IOException e) {
System.err.println(e);
} finally {
try {
in.close(); out.close();
} catch(IOException e) {
System.err.println(e);
}
}
}
public static void main (String args []) {
if (args.length == 1)
new Grep(System.out, args[0], System.in).run();
else
System.err.println("usage: Grep word ...");
}
}
Ein DataInputStream dient zum Aufspalten in Zeilen durch readLine().
Ein DataOutputStream könnte zwar mit writeChars() einen String als Zeile schreiben,
aber das Resultat ist nicht zu readLine() kompatibel.
Aufrufe von close() sind nötig, damit eine Pipeline korrekt funktioniert.
Da Grep das Interface Runnable implementiert, kann ein Grep-Objekt als Thread
2-59
eingesetzt werden.
import java.io.*;
class GrepAll {
public static void main (String args []) {
if (args.length > 0)
try {
InputStream in = System.in;
for (int n = 0; n < args.length-1; ++ n) {
PipedOutputStream out = new PipedOutputStream();
new Thread(new Grep(out, args[n], in)).start();
out.connect((PipedInputStream)
(in = new PipedInputStream(out)));
}
new Grep(System.out, args[args.length-1], in).run();
} catch(Exception e) {
System.err.println(e);
}
else
System.err.println("usage: GrepAll pat ...");
}
}
Bis zum vorletzten Objekt werden Grep-Objekte in Threads aktiviert, zwischen denen
jeweils ein PipedInputStream und ein PipedOutputStream zur Kommunikation dienen.
Diese Objekte können in beliebiger Reihenfolge erzeugt werden; sie werden entweder
per Konstruktor oder per connect() verknüpft.
Das letzte Grep-Objekt wird wie in Grep in main() selbst als Scanner verwendet.
2-60
2-61
2-62
2-63
2-64
Herunterladen