62 Copyright 1996-1997 by Axel T. Schreiner. All Rights Reserved. Verschachtelte und anonyme Klassen Im ersten Beispiel modellieren mehrfach verschachtelte Klassen eine konventionellere Lösung mit verschachtelten Funktionen. Anonyme Klassen spezialisieren abstrakte Klassen, so daß sie instantiiert werden können. Das zweite Beispiel zeigt, wie man die Argumente der Kommandozeile als StringReader bearbeiten kann. Ein Programm kann dann zum Beispiel wahlweise Eingabezeilen oder Kommandoargumente mit den gleichen Methoden verarbeiten. Arithmetische Ausdrücke bewerten — char/Expression Expression liest Zeilen mit arithmetischen Ausdrücken von der Standard-Eingabe und bewertet sie mit float-Arithmetik. Expression demonstriert den Umgang mit StreamTokenizer zur Analyse eines Texts. Expression erweitert Number und erlaubt damit den Einsatz von Ausdrücken an Stelle von Konstanten. {programs/char/Expression.java} import java.io.*; /** A class to store and evaluate arithmetic expressions */ public abstract class Expression extends Number { /** reads lines from stdin and evaluates them */ public static void main (String args []) { StreamTokenizer s = new StreamTokenizer(new InputStreamReader(System.in)); s.commentChar(’#’); // ignore # to eol do try { Number n = Expression.parse(s); if (n != null) System.out.println(n.floatValue()); } catch (java.lang.Exception e) { System.err.println(s +": "+ e); try { while (s.nextToken() != s.TT_EOF && s.ttype != s.TT_EOL) ; } catch (IOException ioe) { } } while (s.ttype == s.TT_EOL); } {} sollte auf einen Reader aufgesetzt werden und gibt dann Symbole als in ttype ab. Das Verhalten kann durch Definition verschiedener Zeichenklassen beeinflußt werden, aber es eignet sich wohl am besten zur Zerlegung einer Art Programmiersprache. StreamTokenizer nextToken() und 63 Fehler {programs/char/Expression.java} /** A class to describe parsing exceptions */ public static class Exception extends java.lang.Exception { public Exception (String msg) { super(msg); } } {} Bei Fehlern wird abgebrochen; der Aufrufer kann dann zum Beispiel den Rest einer Eingabezeile beseitigen. Die Fehler werden mit einer neuen, verschachtelt definierten Exception-Klasse berichtet. StreamTokenizer liefert als toString() eine Positionsangabe. Arithmetik {programs/char/Expression.java} /** map arithmetic to preferred types */ public byte byteValue () { return (byte)intValue(); } public double doubleValue () { return floatValue(); } public long longValue () { return intValue(); } public short shortValue () { return (short)intValue(); } {} Expression soll einen Ausdruck als Baum speichern und sich wie eine Number bewerten lassen. Um die Zahl der nötigen Methoden zu reduzieren, soll nur in int und float gerechnet werden. Die anderen Methoden, die eine Unterklasse von Number implementieren muß, werden für alle Unterklassen umgelenkt. Expression ist noch immer abstract, denn intValue() und floatValue() fehlen. 64 Zeilen {programs/char/Expression.java} /** expr: sum \n */ public static Number parse (StreamTokenizer s) throws Exception, IOException { s.eolIsSignificant(true); for (;;) switch (s.nextToken()) { default: Number result = Sum.parse(s); if (s.ttype != s.TT_EOL) throw new Exception("expecting nl"); return result; case s.TT_EOL: continue; case s.TT_EOF: return null; } } {} Expression.parse() soll eine Summe von einer Zeile speichern. Damit das Ende der Eingabe als null berichtet werden kann, ist parse() nicht als Konstruktor ausgeführt. Da Expression eigentlich nur die Ableitung von Number vereinfacht, sind alle Resultate Numberund nicht Expression-Objekte. Außerdem kann man dann als Faktor direkt eine Number liefern. 65 Verschachtelung {programs/char/Expression.java Sum} /** sum: product { +- product } */ protected abstract static class Sum extends Expression { protected Number left, right; protected Sum (Number left, Number right) { this.left = left; this.right = right; } {} {programs/char/Expression.java product} /** product: term { *%/ term } */ protected abstract static class Product extends Sum { protected Product (Number left, Number right) { super(left, right); } {} {programs/char/Expression.java term} } // end Product } // end Sum /** term: +term | -term | (sum) | number */ protected abstract static class Term extends Expression { protected Number arg; protected Term (Number arg) { this.arg = arg; } {} {programs/char/Expression.java wrap} } // end Term } // end Expression {} Expression enthält Sum — Knoten mit zwei Abkömmlingen — und Term — Knoten mit einem Abkömmling. Sum enthält Product. Alle diese Klassen sind abstract — erst Spezialisierungen für die Operatoren implementieren die fehlenden Methoden. Objekte können hier nur in geschachtelten Unterklassen oder lokal mit parse() erzeugt werden. Da die Klassen protected sind, kann auch parse() trotz public nicht verschleppt werden. 66 Summe erkennen und bewerten {programs/char/Expression.java Sum} /** sum: product { +- product } */ public static Number parse (StreamTokenizer s) throws Exception, IOException { s.ordinaryChar(’-’); s.ordinaryChar(’/’); Number result = Product.parse(s); for (;;) switch (s.ttype) { case ’+’: s.nextToken(); result = new Sum(result, Product.parse(s)) { public float floatValue () { return left.floatValue() + right.floatValue(); } public int intValue () { return left.intValue() + right.intValue(); } }; continue; case ’-’: s.nextToken(); result = new Sum(result, Product.parse(s)) { public float floatValue () { return left.floatValue() - right.floatValue(); } public int intValue () { return left.intValue() - right.intValue(); } }; continue; default: return result; } } {} Addition und Subtraktion unterscheiden sich nur in der Bewertung, folglich werden anonyme Klassen zur Modellierung verwendet. Konstruktoren darf man in anonymen Klassen nicht ersetzen, aber das ist hier auch nicht nötig. In den Quellen sieht man, daß StreamTokenizer das Zeichen / für Kommentare parseNumbers() vergibt. Das muß man hier rückgängig machen. und - durch 67 Produkt {programs/char/Expression.java product} /** product: term { *%/ term } */ public static Number parse (StreamTokenizer s) throws Exception, IOException { Number result = Term.parse(s); for (;;) switch (s.ttype) { case ’*’: s.nextToken(); result = new Product(result, Term.parse(s)) { public float floatValue () { return left.floatValue() * right.floatValue(); } public int intValue () { return left.intValue() * right.intValue(); } }; continue; case ’/’: s.nextToken(); result = new Product(result, Term.parse(s)) { public float floatValue () { return left.floatValue() / right.floatValue(); } public int intValue () { return left.intValue() / right.intValue(); } }; continue; case ’%’: s.nextToken(); result = new Product(result, Term.parse(s)) { public float floatValue () { return left.floatValue() % right.floatValue(); } public int intValue () { return left.intValue() % right.intValue(); } }; continue; default: return result; } } {} 68 Term {programs/char/Expression.java term} /** term: +term | -term | (sum) | number */ public static Number parse (StreamTokenizer s) throws Exception, IOException { switch (s.ttype) { case ’+’: s.nextToken(); return Term.parse(s); case ’-’: s.nextToken(); return new Term(Term.parse(s)) { public float floatValue () { return - arg.floatValue(); } public int intValue () { return - arg.intValue(); } }; case ’(’: s.nextToken(); Number result = Sum.parse(s); if (s.ttype != ’)’) throw new Exception("expecting )"); s.nextToken(); return result; case s.TT_NUMBER: result = s.nval == (int)s.nval ? (Number)new Integer((int)s.nval) : (Number)new Float(s.nval); s.nextToken(); return result; } throw new Exception("missing term"); } {} + wird einfach ignoriert, - als Vorzeichen mit einer anonymen Klasse implementiert. Klammern führen zur Rekursion — aber Sum ist ausreichend sichtbar. Leider liefert der StreamTokenizer in nval nur einen double-Wert. Hier wird angedeutet, daß der Expression-Baum durchaus verschiedene Number-Objekte enthalten kann, da Number Wertabfragen in alle arithmetischen Zieltypen verlangt. Java verbindet implizite Umwandlung mit Overloading, deshalb funktioniert die Konstruktion der Float-Number aus dem double-Wert nval. 69 Arithmetische Ausdrücke auf der Kommandozeile — char/Expr Expr erweitert Expression 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 StringReader ein Reader wird, den man mit den Methoden von Expression bearbeiten kann. {programs/char/Expr.java} import java.io.*; /** A class to mimic part of the expr command */ public abstract class Expr extends Expression { /** evaluates one expression on the command line * or expression lines from standard input */ public static void main (String args []) { if (args == null || args.length == 0) Expression.main(args); else { // turn command line into Reader StringBuffer b = new StringBuffer(args[0]); for (int n = 1; n < args.length; ++ n) b.append(" ").append(args[n]); b.append("\n"); StreamTokenizer s = new StreamTokenizer(new StringReader(b.toString())); try { Number n = parse(s); if (n != null) System.out.println(n.intValue()); } catch (java.lang.Exception e) { System.err.println(s +": "+ e); } } } } {} Expr.java befindet sich im gleichen Katalog und anonymen Paket wie Expression.java, deshalb muß nicht importiert werden. Klassenmethoden wie main() und parse() werden vererbt, aber man kann sie auch über die Klasse aufrufen. super kann man in einer Klassenmethode — ebenso wie this — nicht verwenden, da eine Klassenmethode anders als in Objective C keinen Empfänger hat. Der einzige Effekt der Vererbung ist, daß man parse() direkt und nicht als Expression.parse() aufrufen kann. Obgleich Expr noch abstract ist, kann man main() und parse() verwenden.