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