a> |
In fast allen Programmen kann es an diversen Stellen zu Situationen kommen, die den normalen Programmablauf unterbrechen. Solche "Ausnahmesituationen" treten beispielsweise auf, wenn nicht genügend Speicher bereitgestellt werden kann, oder wenn eine Datei nicht geöffnet werden kann. Es sind also Probleme, die erst zur Laufzeit des Programmes auftreten.
Mit dem von Java unterstützten Exception-Handling kann man auf Fehler in sehr komplizierten Situationen einfach und auf sehr differenzierte Art und Weise reagieren. Der eigentliche Programmcode wird von den vielen einzelnen Fehlerabfragen gesäubert.
Tritt innerhalb einer Methode eine solche Ausnahmesituation auf, kann der Programmierer an dieser Stelle eine Exception "auswerfen" (Viele interne Methoden von Java nutzen diesen Mechanismus in Fehlerfällen). Die Folge des Auswerfens einer Exception ist, daß der Programmablauf an dieser Stelle unterbrochen wird und an einer Abfangstelle innerhalb des Programmes fortgesetzt wird. Die Abfangstelle muß vom Programmierer vorher festgelegt werden. Dies geschieht folgendermaßen: Wird innerhalb eines Programmes eine Methode aufgerufen, die eine Exception auswerfen kann, muß diese Methode in einer Abfangumgebung eingeschlossen werden.
Um eine Exception auszuwerfen, kann die throw-Anweisung benutzt werden. Der throw-Anweisung muß ein Exception-Objekt folgen. Es gibt eine Reihe von vordefinierten Exception-Klassen, man kann aber auch eigene Exceptions definieren. Diese müssen von der Klasse Throwable abgeleitet werden. Methoden, die eine Excption auswerfen müssen dies bei der Deklaration der Methode im Methodenkopf mit angeben. Dies geschieht durch das Schlüsselwort throws gefolgt vom Typ der ausgeworfenen Exception. Es folgt ein Beispiel:
class MyException extends Exception { string message; public MyException(string _message) { message= _message; } public String toString() { return ("MyException: " + message"); } } void mymethod() throws MyException { ... if ("something went wrong") throw new MyException("something went wrong"); ... }
Um die Methode mymethod benutzen zu können, muß ihr Aufruf in eine try-catch Umgebung eingeschlossen werden:
... try { mymethod(); } catch (MyException e) { System.out.println(e); }
Praktisch alle IO-Operationen von Java werfen eine IOException, so daß Blöcke mit Ein-/Ausgabe-Aufrufen immer in ein entsprechendes try/catch eingeschlossen werden müssen:
import java.io.*; public class IOBeispiel { public static void main(String[] args) { System.out.println("IO-Beispiel\n"); String zeile= ""; int wert; BufferedReader in= new BufferedReader(new InputStreamReader(System.in) ); try { zeile= in.readLine(); } catch (IOException ioe) {} wert= Integer.parseInt(zeile); System.out.println("wert= " + wert); }; };
In Java werden für die Ein- und Ausgabe sogenannte Streams benutzt. Streams dienen zur Umwandlung von Objekten beliebiger Typen in einen Strom aus ASCII-Zeichen (oder Bytes) und umgekehrt. Die Hauptanwendung liegt dementsprechend natürlich bei der zeichenorientierten (oder auch binären) Ein- und Ausgabe; sie stellen also die Schnittstelle des Programms zum Dateisystem des Rechners dar.
Beispielsweise dient ein Stream also auch dazu, eine Zahl inform von ASCII-Zeichen in eine Datei zu schreiben, bzw. aus einer Datei auszulesen. In Java (wie auch in vielen anderen Programmiersprachen) werden sogenannte Standardeingabe- und Standardausgabe-Streams definiert:
Die wesentlichen beteiligten Klassen für Streams und deren Exceptions finden sich im Package java.io. Es gibt es zwei abstrakte Basisklassen InputStream und OutputStream. Diese definieren die Mindestfunktionalität eines Eingabe- bzw. Ausgabestromes. Mit anderen Worten: sie deklarieren eine Reihe von Methoden (wie z.B. read() - zum Einlesen eines einzelnen Zeichens ), die von spezialisierten Klassen definiert und um weitere Funktionen erweitert werden. Die wichtigsten spezialisierten Klassen sind:
Die oben genannten Klassen dienen zum (binären) Einlesen bzw. Ausgeben, d.h. deren Methoden operieren mit Bytes. Glücklicherweise gibt es für die meisten der angegebenen Stream-Klassen aber ab Java 1.2 Versionen namens "-Reader" bzw. "-Writer". Sie sind auf die Ein-/Ausgabe von Zeichen (statt Bytes) spezialisiert. Solche Reader bilden gewissermaßen eine Zwischenschicht zwischen den Stream-Klassen und der Anwendung. Bei der Initialisierung muß ein Stream-Objekt übergeben werden aus dem gelesen bzw. in das geschrieben werden kann. Der Reader bildet dann die "binären" Daten in Unicode-Zeichen ab.
Für zeichenorientierte Dateien sollte man deshalb vorsichtshalber immer Reader und Writer verwenden, und normale Streams nur für binäre Ein- und Ausgaben.
Zum Lesen von Zeichen aus Dateien, benutzt man am besten die Klasse InputStreamReader oder BufferedInputStreamReader. Der nachfolgende Code-Auszug zeigt, wie man zeichenweise aus der Standardeingabe ausliest:
InputStreamReader isr=new InputStreamReader(System.in); int i; while ((i=isr.read())>=0) { System.out.println("gelesenes Zeichen: "+(char)i); }
In diesem Code-Auszug wird zunächst ein InputStreamReader erzeugt, und mit der Standard-Eingabe verbunden (1. Code-Zeile). Anschließend werden Daten aus der Standardeingabe Zeichenweise gelesen (While-Schleife), bis keine Daten mehr in der Standardeingabe vorhanden sind.
Zur Lösung des Übungsblattes Nr. 4 (und vieler weiterer Übungsblätter) ist es notwendig, Daten von der Tastatur entgegenzunehmen bzw. aus einer Datei auszulesen.
Diese Operationen unterscheiden sich vom Auslesen der Kommandozeilenparameter! Kommandozeilenparameter werden beim Programmstart angegeben, Eingabeoperationen verlaufen interaktiv im Verlauf des Programmablaufes.
Leider gibt es keine Möglichkeit, aus ASCII-Files beispielsweise einzelne int- oder double-Zahlen zu lesen. Dazu muß man eine Zeile als String einlesen, selbst in Teile zerlegen (z.B. mit einem StringTokenizer) und selbst die Umwandlung in den gewünschten Typ vornehmen. Dabei ist entweder die Klasse NumberFormat oder die jeweilige Wrapper-Klasse hilfreich:
String s; // ... Einlesen aus der Datei nach s double d=Double.valueOf(s).doubleValue();
Das nachfolgende Java-Programm liest eine Integer-Zahl ein und gibt diese anschließend wieder aus:
import java.io.*; public class IOBeispiel { public static void main(String[] args) { System.out.println("IO-Beispiel\n"); String zeile= ""; int wert; BufferedReader in= new BufferedReader(new InputStreamReader(System.in) ); try { zeile= in.readLine(); } catch (IOException ioe) {} wert= Integer.parseInt(zeile); System.out.println("wert= " + wert); }; };
Wird das Programm gestartet, so wird der String "IO-Beispiel" auf dem Bildschirm ausgegeben. Anschließend wartet das Programm auf eine Eingabe des Benutzers, der nun kann eine Zahl eingegeben kann (Abschluß der Eingabe mit der Return-Taste). Die Eingabe wird zunächst in der String-Variablen zeile abgelegt und anschließend mit Hilfe von Integer.parseInt(zeile) wieder ausgegeben. Falls die Eingabe kein Integerwerten ist, wird das Programm mit einem Fehler (Exception) abgebrochen.
Beispielaufruf (unterstrichener Text spiegelt Tastatureingaben wieder):
> java IOBeispiel IO-Beispiel 1234 wert= 1234
Es sei noch einmal darauf hingewiesen, daß es zum Lösen der Übungsaufgaben nicht notwendig ist, genau zu verstehen, was sich hinter einem BufferedReader-Objekt verbirgt! Das Beispiel sollte jedoch vergleichsweise leicht zum Lösen der Übungsaufgaben angepaßt werden können.
public String readLine() throws IOException
Die Eingabe von Daten aus einer Datei erfolgt in ähnlicher Weise wie die Eingabe von der Tastatur. Es muß lediglich zuerst die Datei geöffnet werden, aus der die Daten ausgelesen werden sollen. Dies geschieht, indem man ein FileReader-Objekt erzeugt. Bei der Initialisierung dieses Objektes muß der Name der Datei angegeben werden, die geöffnet werden soll.
Das nachfolgende Beispiel liest 64 Integerwerte aus einer Datei aus, deren Name per Kommandozeile übergeben wird. Die eingelesenen Werte werden sofort wieder ausgegeben.
import java.io.*; import java.util.StringTokenizer; public class IOBeispiel { public static void main(String[] args) { String zeile= ""; int wert; FileReader fr= null; BufferedReader in= null; try { fr= new FileReader(args[0]); in= new BufferedReader(fr); } catch(FileNotFoundException fnfe) {} for (int i= 0; i < 8; i++) { try { zeile= in.readLine(); } catch (IOException ioe) {} StringTokenizer st= new StringTokenizer(zeile); for (int j= 0; j < 8; j++) { String s= st.nextToken(); wert= Integer.parseInt(s); System.out.print(wert + "\t"); } System.out.println(""); } }; };
Beispielaufruf (unterstrichener Text spiegelt Tastatureingaben wieder):
> java IOBeispiel test.txt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
Ein StringTokenizer Objekt ermöglicht es, einen String in sogenannte Tokens zu zerlegen. Ein Token besteht aus einer beliebigen Folge von Buchstaben, Zahlen und Sonderzeichen. Die einzelnen Token sind durch sogenannte Delimeter getrennt. Default-Delimeter sind beispielsweise das Leerzeichen (´ ´), das Tabulator-Zeichen (´\t´) und der Zeilenumbruch (´\n´).
Es folgt ein Anwendungsbeispiel des String-Tokenizers:StringTokenizer st = new StringTokenizer("this is a test"); while (st.hasMoreTokens()) { println(st.nextToken()); }Der obige Code-Auszug produziert folgende Ausgabe:
this is a testWeitere Informationen zum StringTokenizer finden sich in der API-Dokumentation von Java-Soft.