Von Enno Runne und Hendrik C.R. Lock
Schon im letzten Kursteil sind uns einige Fälle begegnet, in denen Fehler während des Programmlaufes entstehen konnten. Diese Laufzeitfehler entstehen nicht nur durch logische Programmfehler, sondern werden auch durch äußere Einflüsse hervorgerufen, z.B. wenn eine Datei nicht vorhanden oder nicht beschreibbar ist. Viele solcher Fehler können durch umfangreiche Sicherheitsabfragen vermieden werden und führen dann nicht zum Programmabbruch. Durch das Konzept der Ausnahmebehandlung vereinfacht Java die Realisierung von Sicherheitsabfragen b.z.w. der Reaktion auf fehlerhafte Programmzustände. Wir beginnen diesen Kursteil mit der Ausnahmebehandlung und führen dann die Ein- und Ausgabe ein, die sich wiederum auf das Konzept der Ausnahmen abstützt.
Laufzeitfehler können in den unterschiedlichsten Situationen entstehen. Dabei gibt es Fehler, die eigentlich nicht unbedingt ein Problem darstellen und andere, die so schwerwiegend sind, dass eine Fortsetzung des Programmes nicht mehr sinnvoll ist. Es wird aber zunehmend wichtiger, sichere Programme zu schreiben, die möglichst alle Fehlersituationen handhaben, ohne einen Programmabbruch zu erzwingen. Deshalb bietet Java einen Mechanismus an, um Laufzeitfehler abzufangen und per Programm zu behandeln. Dieser Mechanismus heißt Ausnahmebehandlung.
Laufzeitfehler werden in Java bestimmten Fehlerklassen zugeordnet. Die wichtigste Unterscheidung erfolgt zwischen den Kategorien Fehler (engl. error) und Ausnahmen (engl. exception). In die erste Kategorie fallen alle schwer behebbaren Fehler. Die zweite Kategorie beinhaltet "leichtere" Fehler. Die Fehlerklassen werden in Form eines Vererbungsbaumes weiter unterteilt. Zum Beispiel gehören beide Fehlerarten "Ein-/Ausgabe-Ausnahme" und "Datei nicht vorhanden" zur Kategorie der Ausnahme, und die zweite ist eine Verfeinerung der ersten, steht also im Klassifikationsbaum darunter.
Die Idee der Ausnahmebehandlung in Java ist es, zunächst ein Objekt zu generieren, das alle für die Behandlung dieser Ausnahme benötigten Daten speichert. Wird zur Laufzeit eine Ausnahme ausgelöst, so sucht die virtuelle Java-Maschine (JVM) eine Programmstelle, an der diese Ausnahmesituation bearbeitet wird. Dort wird dann das Ausnahmeobjekt empfangen. In dieser Weise werden also relevante Daten in einer Art Behälter übertragen. Um ein spezifisches Ausnahmeobjekt zu erzeugen, brauchen wir natürlich eine geeignete Klasse. Es ist daher naheliegend, jede Form einer Ausnahmesituation durch eine eigene Klasse anzuzeigen. Gleichartige Ausnahmen werden dann durch Objekte derselben Klasse realisiert. Damit kommen wir zur Klassifikation der Ausnahmen zurück. Wie bereits erwähnt bilden sie einen Baum. Da wir ja Ausnahmetypen mit Klassen darstellen, stellen wir gleichermaßen den Klassifikationsbaum auf eine Klassenhierarchie dar. Somit entspricht jede Verfeinerung in der Fehlerklassifikation einer Vererbungsbeziehung in der Klassenhierarchie. Ein Ausschnitt der Klassenhierarchie von in Java fest eingebauten Ausnahmetypen findet sich im Schaubild 1.
Schaubild 1, bitte einfuegen,
Ueberschrift: Klassenhierarchie der Ausnahmen
Throwable /\ / \ Error Exception /\ / \ IOException RuntimeException /\ /\ FileNotFoundException ... / \ NullPointerException ArrayIndexOutOfBoundsException
Alle Ausnahmen
und Fehler werden durch ein Objekt einer Unterklasse der
Klasse Throwable
angezeigt.
Von ihr leiten
sich Error
und Exception
ab (siehe auch Schaubild 1).
Unterklassen
von Error
sind wie gesagt ernsthafte Fehler, die nicht
so einfach beseitigt werden können. Unter den Nachfahren von
Exception
sind wiederum zwei bedeutende Gruppen:
IOException
und RuntimeException
.
IOException
meldet Ausnahmen bei der Ein- oder Ausgabe.
RuntimeException
hat eine besondere Bedeutung:
sie steht für Fehler, die überall auftreten können.
Um sich die endlose Wiederholung von
throws RuntimeException
zu ersparen, wird in Java
allen Anweisungen unterstellt, dass sie eine RuntimeException
auslösen können.
Ein Beispiel hierfür ist die
NullPointerException
. Sie wird immer dann ausgelöst,
wenn über eine uninitialisierte Referenz ein Methodenaufruf stattfindet
(wir erinnern, dass uninitialiserte Referenzen den Wert null
enthalten).
Deshalb kann dieser Ausnahmetyp im Prinzip
in den meisten Anweisungen entstehen - eine Angabe, dass dies so ist,
wäre unnötige Schreibarbeit.
Wie wir sehen, ist die Ausnahme "Datei nicht vorhanden"
eine Verfeinerung der "Ein-/Ausgabe-Ausnahme",
was durch Vererbung ausgedrückt wird.
Wird nun zum Beispiel auf eine nicht existierende Datei zugegriffen,
so erzeugt die JVM automatisch ein Objekt der Klasse
FileNotFoundException
und löst eine Ausnahmebehandlung aus.
Hier wird der Vorteil der Unterklassen deutlich: Wir können das Fehlen der Datei behandeln, aber alle anderen Ein-/Ausgabefehler einfach unbeachtet lassen. Natürlich bleiben sie nicht unbeachtet: eine Ausnahme, die nicht vom Programm abgefangen wird, wird sofort auf der Ebene der JVM behandelt. Das Programm wird dann mit einer Fehlermeldung abgebrochen, und als Fehlermeldung wird der Inhalt des Ausnahmeobjektes ausgegeben, z.B.:
java.lang.NullPointerException at kurs4Test.main(kurs4Test.java:15)Hier meldet uns die JVM eine Ausnahme bei der Ausführung von Zeile 15 der Datei
kurs4Test.java
, und zwar innerhalb der
Prozedur main()
.
Schaubild 2, fliessend
Ueberschrift: Die Klassen Throwable
und Exception
public class java.lang.Throwable extends java.lang.Object { // Constructors public Throwable(); public Throwable(String message); // Methods public Throwable fillInStackTrace(); public void printStackTrace(); public void printStackTrace(PrintStream s); public String getMessage(); public String toString(); } public class java.lang.Exception extends java.lang.Throwable { // Constructors public Exception(); public Exception(String s); }
AddressException
ein, die wir dann
entsprechend weiter verfeinern (siehe Bild unten).
Exception / / .... AddressException /\ / \ InvalidAgeException AddressNullNameExceptionJede der Unterklassen
InvalidAgeException
und AddressNullNameException
behandelt eine der beiden möglichen Ausnahmen.
Im folgenden Code definieren wir nur die Klasse
InvalidAgeException
, die Definition der anderen Klasse
unterscheidet sich nur in den Namen der Klasse und der Konstruktoren.
public class AddresssException extends Exception {} public class InvalidAgeException extends AddressException { public InvalidAgeException() { super(); } public InvalidAgeException(String s) { super(s); } } public class AddressNullNameException extends AddressException {...}Das war`s auch schon. Eine Ausnahme besitzt normalerweise zwei Konstruktoren, von denen der zweite eine Fehlermeldung als Argument aufnimmt. Der Text dieser Fehlermeldung kann bei einer später erfolgenden Ausnahmebehandlung ausgegeben werden. Diese Konstruktoren werden von der Oberklasse
Exception
geerbt.
Normalerweise besitzen Unterklassen von Exception
keine weiteren Methoden.
Natürlich ist es möglich - und oft auch sinnvoll - der
jeweiligen Ausnahmenklasse noch spezielle Elemente hinzuzufügen, die
dann genauere Informationen
über die Fehlersituation enthalten.
Wir werden jetzt den Konstruktor der Klasse Adresse
so umschreiben,
dass er eine Ausnahme auszulöst, wenn ein negatives Alter angegeben
wird.
Adresse(String vorname, String nachname, int alter) throws InvalidAgeException { this.vorname = vorname; this.name = nachname; if (alter < 0) throw new InvalidAgeException( "Das Alter darf nicht negativ sein!"); this.alter = alter; }Jede Methode muss mitteilen, welche Ausnahmen sie erzeugen kann. Dies geschieht durch das Schluesselwort
throws
in der Methodendeklaration. Auf throws
folgt eine Liste aller Ausnahmen, mit denen der Benutzer dieser
Methode rechnen muss. In unserem Fall geben wir
bekannt, dass der Konstruktor die
Ausnahme InvalidAgeException
auslösen kann.
Im Falle einer Liste von Ausnahmen werden die Klassennamen
syntaktisch durch Kommata getrennt.
Der nächste Schritt besteht darin, eine Ausnahme auch tatsächlich
auszulösen. Dieser Fall tritt ein, wenn das angegebene Alter negativ ist.
Wir erzwingen eine Ausnahmebehandlung, indem wir das Schlüsselwort
throw
einsetzen, gefolgt von der Referenz eines Objektes
des entsprechenden Ausnahmetyps. Im Allgemeinen treffen wir
dabei die folgende
Anweisung an.
throw new TypDerAusnahme( Text );Es ist zu beachten, dass der Typ der Ausnahme auch tatsächlich in der Liste der möglichen Ausnahmen der Methodendeklaration auftritt, oder zu einer Unterklasse der genannten Klassen gehört. Eine Abweichung von dieser Regel bilden alle Ausnahmen, die durch Unterklassen des Typs
RuntimeException
gekennzeichnet sind.
Die throw
-Anweisung hat zwei Aufgaben:
Um unser Programm in seiner Ausfährung sicher zu gestalten,
mässen wir nun alle Programmteile, die das Element Alter
ändern, absichern.
Entsprechend wird auch die Methode
setzeAlter
im Falle eines negativen Alters
die Ausnahme InvalidAgeException
auslösen.
void setzeAlter(int alter) throws InvalidAgeException { if (alter < 0) throw new InvalidAgeException( "Das Alter darf nicht negativ sein!"); this.alter = alter; }
Die Grundidee bei der Ausnahmebehandlung
ist es, die Quelle einer möglichen Ausnahme
einzufassen. Eine solche Quelle kann eine Anweisungsfolge,
eine einzelne Anweisung, und ein Methodenaufruf sein.
Die Quelle wird mit dem syntaktischen Konstrukt try
eingefasst. Es wird also versucht, eine Anweisungsfolge auszuführen.
try
signalisiert, dass dieser Versuch fehlschlagen kann.
Das heißt, dass die Ausfährung möglicherweise
durch eine Ausnahme unterbrochen wird.
Dem try
-Konstrukt folgt eine Beschreibung der Ausnahmen,
die abgefangen werden sollen. Alle anderen aus der Quelle
austretenden Ausnahmen werden an der momentanen
Programmstelle ignoriert und weitergereicht.
Im Detail teilt sich die Behandlung einer Ausnahme in drei Bereiche auf. Jeder dieser Bereiche ist ein Anweisungsblock.
try
-Block: Er enthält diejenigen Anweisungen,
die die Quelle einer möglichen Ausnahme darstellen.
Dies schließt direkte throw
-Anweisungen ein.
catch
-Block: Hier wird durch Angabe
des jeweiligen Typs angegeben, welche Ausnahmen an dieser Stelle
behandelt werden. Jede Ausnahme benötigt genau einen
catch
-Block. Mehrere Ausnahmen werden durch
aufeinanderfolgende Blöcke aufgefangen.
finally
-Block: Hier stehen Anweisungen, die auf jeden
Fall (egal ob Fehler oder nicht) ausgeführt werden.
try
-Block
und mindestem einem catch
-Block.
Der finally
-Block ist optional.
Wir stellen nun diese Komponenten im Detail vor.
Der try
-Bereich enthält eine Anweisungsfolge.
try { Anweisung1; Anweisung2; Anweisung3; } catch (...) { ... } Anweisung4;Nehmen wir einmal an, zur Laufzeit würde die zweite Anweisung durch einen Fehler vorzeitig beendet, also z.B. durch eine
throw
-Anweisung.
Dann wird auch die Abarbeitung der Anweisungsfolge im Block abgebrochen,
d.h. keine der nachfolgenden Anweisungen wird ausgeführt. In unserem Fall
wird die dritte Anweisung ausgelassen.
Das Programm versucht dann, in einem der nachfolgenden
catch
-Anweisungen wiederaufzusetzen.
Kann also ein passendes catch
gefunden werden, so werden auch
die nachfolgenden Anweisungen ausgeführt, in unserem Beispiel
ist dies Anweisung4
.
Passt kein catch
, so wird auch hier abgebrochen und die Ausnahme somit an
den Aufrufer weitergereicht.
In diesem Fall wird also Anweisung4
nicht mehr ausgeführt. Steht vor Anweisung4
ein finally
-Block,
so wird dessen Anweisungsteil auf jeden Fall noch ausgeführt.
Ein catch
-Block besteht aus
3 Komponenten: dem Schlüsselwort, der Deklaration eines Bezeichners
für den Behälter und einem Anweisungsblock.
Der Typ dieses Bezeichners ist ein Klassenname.
Wie wir bereits wissen, werden Ausnahmen durch Klassen vereinbart.
Das heißt aber, dass dieser Name zugleich auch die Ausnahme benennt, die
hier abgefangen und behandelt werden soll.
catch (TypDerAusnahme ausnahme) { ... // Behandlung }Jede Ausnahme wird mittels eines Objektes gemeldet. Die Klasse dieses Objektes wird durch die Ausnahmeursache festgelegt, wie wir bereits bei der
throw
-Anweisung gesehen haben.
Eine Ausnahme passt also auf ein catch
, wenn die Klasse des
Ausnahmeobjektes typkonform zur Klasse der catch
-Anweiseung ist,
d.h. wenn es von dieser oder einer Unterklasse ist.
Passt der Catch, so wird das Ausnahmeobjekt an den Bezeichner zugewiesen
und der Anweisungsteil ausgeführt.
Alle weiteren catch
-Anweisungen werden anschließend
übersprungen.
Passt die Klasse beim ersten catch
nicht, so
wird die nachfolgende catch
-Anweisung untersucht, und so fort.
Passt keine Klasse, so wird die gesamte Anweisungsfolge abgebrochen
und die Ausnahme an den Aufrufer weitergereicht.
Die JVM ruft ein Benutzerprogramm sozusagen in einem
try
-Block auf. Dadurch kann sie jeden Fehler, den das
Programm nicht abfängt selbst abfangen. Die Standardreaktion in diesem Fall ist
die Ausgabe eines Fehlertextes (sofern vorhanden)
über die Methode ausnahme.getMessage()
.
Auch innerhalb eines catch
-Blocks kann wieder eine Ausnahme ausgelöst
werden. In diesem Fall wird die Abarbeitung wie bei einer normalen
Anweisung vorzeitig beendet und eine Ausnahmebehandlung in der
nächsthöheren Ebene ausgelöst.
Die nachfolgenden catch
-Blöcke werden also dann übergangen.
Wie sieht es mit fianlly aus?
Wie sieht es bei ueberlappenden Catches aus?
>
Wir wenden nun die neugewonnen Kenntnisse auf unsere Adresskartei an.
try { Adresse adr = new Adresse("Fritz", "Müller"); adr.setzeAlter(-5); } catch (AdressException e) { System.out.println(e.getMessage()); }Wir provozieren durch Angabe eines negativen Alters eine Ausnahmebehandlung. Die im Methodenaufruf auftretende
throw
-Anweisung generiert deshalb ein Objekt des Typs
InvalidAgeException
.
Java bricht nun den Methodenaufruf ab und taucht aus dem try
-Block auf.
Das nachfolgende catch
passt, weil AdressException
eine Oberklasse des tatsächlichen Ausnahmetyps InvalidAgeException
ist.
Die damit angestoßene Ausnahmebehandlung holt sich mittels
e.getMessage();die Fehlermeldung und gibt sie aus.
Hier wird deutlich, wie praktisch die Klassenhierachie für die Ausnahmen ist. Wir können Spezialfälle behandeln oder einfach eine ganze Familie von Ausnahmen. Eine Familie wird durch Angabe einer geeigneten Oberklasse abgefangen, im Beispiel werden alle mit dem Setzen von Adressen zusammenhängenden Fehler zusammen behandelt.
Tritt beim Belegen der Adresse jedoch eine andere Fehlerform auf,
z.B. eine NullPointerException
,
so würde dies hier
zum Programmabbruch führen, da keine Behandlung vorgesehen ist.
Wir reagieren bisher mit einer Fehlermeldung.
Jetzt wollen wir zusätzlich die Adresse ausgeben, und zwar unabhängig davon,
ob die Alterszuweisung erfolgreich oder fehlerhaft war.
Wir erreichen dies durch Hinzunahme einer finally
-Anweisung,
denn diese wird in jedem Fall ausgeführt.
.... finally { adr.ausgeben(); }
Besonders wichtig sind Ausnahmen selbstverständlich für die Zusammenarbeit mit dem Betriebssystem, und dabei speziell bei der Ein- und Ausgabe. Dies ist auch unser naechstes Thema.
Java arbeitet bei allen Ein- und Ausgaben mit Strömen (engl. stream).
Die dazu notwendigen Klassen finden sich alle in der Standardbibliothek
java.io
. Eingaben wie Ausgaben werden jeweils mit
Hilfe von Grundklassen dem Programm zugeführt.
Jede Grundklasse steht für Ein- oder Ausgabe auf
ein bestimmtes Gerät oder mit bestimmten Datenstrukturen.
Die Klasse InputStream
(und jeweils analog OutputStream
)
definiert als Oberklasse einen Minimalstandard.
Alle Eingabeströme erben rudimentäre Methoden zum Liefern von Daten, die
es erlauben einzelne Bytes zu liefern. Die Unterklassen
ByteArrayInputStream
, FileInputStream
,
PipedInputStream
und
StringBufferInputStream
stehen dabei jeweils für eine bestimmte
Quellart, von der die Daten kommen (s. Tabelle 1).
So wird ein FileInputStream
benutzt, um Daten aus
Dateien zu lesen.
Klassenname | Funktionalität |
InputStream OutputStream |
einfache Ein-/Ausgabe (Oberklassen) |
ByteArrayInputStream ByteArrayOutputStream |
Einlesen aus einem Byte-Feld Ausgeben in ein Byte-Feld |
FileInputStream FileOutputStream |
Lesen aus Dateien Schreiben in Dateien |
PipedInputStream PipedOutputStream |
Datenaustausch zwischen parallel ablaufenden Programmteilen |
StringBufferInputStream |
Einlesen aus einem StringBuffer |
FileInputStream
können wir
also nur einzelne Zeichen aus einer Datei lesen. Damit könnten
wir jetzt eine eigene Eingaberoutine schreiben, was aber
recht aufwendig und zum Glück unnötig ist.
Java bietet uns natürlich auch komfortablere Klassen, die z.B. auch zeilenweises Einlesen unterstützen. Dabei benutzt Java ein geschicktes Konzept, um den Programmieraufwand gering zu halten. Die einfachen Ströme lassen sich Verknüpfung mit spezielleren Stromklassen (s. Tabelle 2) verbessern! Ein speziellerer Strom muss aber nicht für jede Art von Ein-/Ausgabegerät getrennt programmiert werden, da er seine Daten jeweils aus einem beliebigen der Grundströme bezieht.
Klassenname | Funktionalität |
BufferedInputStream BufferedOutputStream |
gepuffertes Einlesen gepufferte Ausgabe |
DataInputStream DataOutputStream |
Einlesen und Schreiben der Java-Grunddatentypen für Datendateien |
FilterInputStream FilterOutputStream |
während der Ein- und Ausgabe können die Daten in ein anderes Format gebracht werden |
LineNumberInputStream |
beim Einlesen wird die Zeilennummer gezählt |
PushbackInputStream |
bietet die Möglichkeit ein Zeichen wieder als ungelesen zurückzuschreiben |
PrintStream |
Zeilenweise Ausgabe |
SequenceInputStream |
Verkettung mehrerer Eingabeströme |
Diese Ströme können nun über die Ströme mit minderer Funktionalität "drübergestülpt" werden. Dazu geben wir den einfachen Strom als Quelle (bzw. Ziel) des gewünschten Stromes an. Der spezielle Strom nutzt dann die Ein- bzw. Ausgabemethoden des einfachen Stromes und baut daraus die komfortable Ein- oder Ausgabe auf.
In unserem Fall sollen die einzelnen Zeichen der Tastatur
eingelesen werden und als ganze Zeile
in unserem Programm ankommen. Die Eingaben der Tastatur
werden durch den Strom System.in
an ein Java-Programm
gemeldet. Dies ist "nur" ein einfacher InputStream
.
Die Klasse DataInputStream
erkennt Zeilenenden in
Eingaben und kann so ganze Zeilen als String
an
das Programm weiterliefern. Wir können jetzt den Strom
System.in
als Eingabe für einen DataInputStream
verwenden:
String zeilenweiseEingabe; zeilenweiseEingabe = new DataInputStream(System.in);Der Konstruktor des
DataInputStream
nimmt also als
Parameter einen Eingabestrom, aus dem die einzelnen Zeichen gelesen
werden.
Schematisch können wir uns den Eingabevorgang dann ungefähr so vorstellen:
Quelle einfacher Strom spezieller Strom Programm einzelne ganze Zeile Zeichen von -> InputStream -> DataInputStream -> in einem der Tastatur (liest ein) (erkennt Zeilenende) StringGenauso können wir mit allen Grundklassen von Strömen verfahren. Wollen wir Zeilen aus einer Datei lesen, so nehmen wir einen
FileInputStream
und nutzen ihn als Eingabe für
den DataInputStream
:
String zeileAusDatei; zeileAusDatei = new DataInputStream(new FileInputStream("testdatei.txt"));Der Trick bei der Sache ist nun, dass alle spezielleren Eingabetröme wiederum als Eingabeströme dienen können. Durch Aneinanderreihung ist es möglich die Funktionen verschiedener Klassen zusammenzuführen. So könnten wir das Einlesen aus der Datei beschleunigen, indem wir die Eingaben puffern. Dazu steht der
BufferedInputStream
zur Verwendung bereit.
Er benutzt einen Puffer, der immer komplett "vollgelesen" wird. Dadurch
verringert sich die Zahl der Laufwerkszugriffe. Die Größe des
Puffers können wir bei seiner Erzeugung angeben:
String zeileAusDatei; zeileAusDatei = new DataInputStream( new BufferedInputStream(new FileInputStream("testdatei.txt"), 1024)));Hier haben wir also noch einen Strom zwischengeschaltet, der uns ohne Veränderung genau das leistet, was wir haben wollten. Der klare Vorteil dieser Hintereinanderschaltung ist nun, dass der
BufferedInputStream
genauso für beliebige andere
Eingabequellen benutzt werden kann.
Wir wollen den Benutzer jetzt eine Adresse über die Standardeingabe
(Tastatur) eingeben lassen. Dabei lesen wir die Felder unserer
Adresse als String
ein. Dies organisiert der Strom
DataInputStream
, wie oben beschrieben.
import java.io.*; //... in die Klasse Adresse einfügen... void einlesen() throws IOException { DataInputStream eingabeStrom = new DataInputStream(System.in); System.out.println("Name: "); name = eingabeStrom.readLine(); System.out.println("Vorname: "); vorname = eingabeStrom.readLine(); }Zu Beginn der Quelldatei von
Adresse
müssen wir jetzt
die Bibliothek java.io
komplett einbinden, dies deuten
wir mit java.io.*
an. Alle Klassen, die zur Ein- und Ausgabe
benötigt werden, sind in diesem Paket enthalten.
Unsere Klasse Adresse
erweitern
wir dann um die Methode einlesen()
. Zunächst geben wir
mit throws
vorläufig an, dass alle Ausnahmen, die beim Einlesen entstehen,
an die aufrufende Methode weitergeleitet werden sollen.
Um einen String
von der Standardeingabe einzulesen,
erzeugen wir unseren Eingabestrom von der Klasse
DataInputStream
mit dem Parameter System.in
.
Damit erreichen wir, dass alle Eingaben der Tastatur an
unser Objekt der Klasse DataInputStream
weitergegeben werden.
Sie fügt die Eingaben zusammen zu einem String
und
übergibt uns diesen durch die Methode readLine()
.
readLine()
liest jeweils die Zeichen aus dem
Eingabestrom, bis ein Zeilenende erreicht wird. Hierbei ist bemerkenswert,
dass auch DataInputStream
maschinenunabhängig arbeitet
und bei DOS-Systemen CR
und LF
erwartet,
während unter Unix das CR
ausreicht.
Unsere modifizierte Klasse Adresse
enthält jetzt die
neue Methode zum Einlesen. Allerdings haben wir dort noch kein Alter
eingelesen. Wenn wir uns die Methoden von DataInputStream
ansehen, finden wir dort auch
public final int readInt()als Methode. Sie liefert uns scheinbar ein eingelesenes
int
.
Doch DataInputStream
dient auch zum Lesen von
Dateien in einem systemunabhängigen Format. Deshalb erwartet diese
Klasse genau vier Zeichen, aus denen dann der int
-Wert
berechnet wird. Eine Eingabe von Ziffern würde nicht das erwünschte
Ergebnis liefern.
Wir müssen hier deshalb einen Umweg gehen: Wir lesen das Alter
ebenfalls als String
und versuchen anschließend die
eingegebenen Zeichen in einen Wert zu wandeln. Die Klasse Integer
bietet dazu Methoden an. Sie ist das Klassenpendant zum Grundtyp
int
.
System.out.println("Alter: "); String stringAlter = eingabeStrom.readLine(); Integer tmpAlter = new Integer(-1); setzeAlter(tmpAlter.parseInt(stringAlter));Wir lesen also wie vorher auch eine Zeile als Eingabe und legen sie in
stringAlter
ab. Anschließend erzeugen wir ein
leeres Objekt tmpAlter
der Klasse Integer
.
Die Methode parseInt(String)
wandelt die Eingabe in ein
int
um. Jetzt kommt es aber vor, dass ein Benutzer eine
Eingabe macht, die keine Zahl darstellt. In diesem Fall löst
parseInt
eine NumberFormatException
aus.
Außerdem könnte unsere Methode setzeAlter
die Ausnahme InvalidAgeException
veranlassen.
Wir werden den Benutzer solange Daten eingeben lassen, bis ein sinnvoller
Wert eingegeben ist. Unsere vollständige Klasse sieht dann so aus:
class Adresse { String vorname, name; int alter; Adresse() { super(); } Adresse(String vorname, String nachname, int alter) throws InvalidAgeException { this.vorname = vorname; name = nachname; if (alter < 0) throw new InvalidAgeException( "Das Alter darf nicht negativ sein!"); this.alter = alter; } Adresse(String vorname, String nachname) { this.vorname = vorname; name = nachname; } void einlesen() throws IOException { DataInputStream eingabeStrom = new DataInputStream(System.in); System.out.println("Name: "); name = eingabeStrom.readLine(); System.out.println("Vorname: "); vorname = eingabeStrom.readLine(); boolean fehler; do { fehler = false; System.out.println("Alter: "); String stringAlter = eingabeStrom.readLine(); Integer tmpAlter = new Integer(-1); try { setzeAlter(tmpAlter.parseInt(stringAlter)); } catch (NumberFormatException e) { System.out.println("Bitte eine Zahl eingeben!"); fehler = true; } catch (InvalidAgeException e) { System.out.println("Ein Alter kann nicht negativ sein!"); fehler = true; } } while (fehler); } void ausgeben() { System.out.println(vorname + " " + name); System.out.println("Alter: " +alter ); } public String toString() { return "Adresse (" + name + " " + vorname + " " + alter + ")" ; } }Hier benutzen wir eine
do-while
-Schleife. Sie wird immer
einmal ausgeführt und solange wiederholt, bis fehler
den Wert false
enthält. Im try
-Block
steht die Anweisung, bei deren Ausführung die Ausnahmen ausgelöst
werden könnten. Wir behandeln die beiden Ausnahmen jeweils mit
einer Fehlermeldung. Natürlich können bei der Eingabe auch
andere Ausnahmen entstehen. Sie sind alle Spezialfälle der
IOException
. Solche Ausnahmen behandeln wir in der
Methode einlesen
nicht, sondern geben sie mit throws
weiter.
So können jetzt während des Programmlaufs Adressen eingegeben werden. Unser Hauptprogramm und die Kartei formulieren wir dann so:
class AdressKartei { Vector adrVector = new Vector(16); void einfügen(Adresse adr) { adrVector.addElement( adr ); } void ausgeben() throws ArrayIndexOutOfBoundsException { for (int i = 0; i < adrVector.size(); i++) { Object obj = adrVector.elementAt(i); if (obj instanceof Adresse) { ((Adresse)obj).ausgeben(); }; } } } public class AdressProgramm { public static void main(String argv[]) { AdressKartei kartei = new AdressKartei(); Adresse adr; for (int i=0; i<5; i++) { try { adr = new Adresse(); adr.einlesen(); kartei.einfügen(adr); } catch (IOException e) { System.out.println(e.getMessage()); } } kartei.ausgeben(); } }Im Hauptprogramm behandeln wir jetzt auch die Ein-/Ausgabe-Ausnahmen, indem wir jeweils die Meldung der Ausnahme ausgeben.
Analog zur Eingabe von der Tastatur können wir auch aus einer Datei
einlesen. Dazu benutzen wir nicht den Strom System.in
der
die Eingaben von der Tastatur einliest, sondern erzeugen einen neuen
FileInputStream
. Dies ist mit zwei verschiedenen Konstruktoren
möglich. Zum einen können wir direkt einen Dateinamen angeben oder
aber wir geben ein Objekt der Klasse File
an, das uns auch
Informationen über die Datei liefern kann.
Wir überladen unsere Methode einlesen()
mit der gleichnamigen Methode einlesen(DataInputStream)
.
Überladen bedeutet, dass mehrere gleichnamige Methoden existieren,
die sich nur durch die Typen oder die Anzahl der Parameter unterscheiden.
Anhand der Parameter wird immer die korrekte Methode gefunden.
void einlesen(DataInputStream eingabeStrom) throws IOException { name = eingabeStrom.readLine(); vorname = eingabeStrom.readLine(); String stringAlter = eingabeStrom.readLine(); if (stringAlter == null) throw new EOFException(); Integer tmpAlter = new Integer(-1); try { int i = tmpAlter.parseInt(stringAlter); setzeAlter(i); } catch (NumberFormatException e) { System.out.println("Ungültige Zahl gelesen!"); } catch (InvalidAgeException e) { System.out.println("Negativer Wert gelesen!"); } }Diese Methode liest Zeile für Zeile aus dem übergebenen Strom und betrachtet immer den Inhalt einer Zeile als Wert für das entsprechende Feld. Falls beim Versuch das Alter zu lesen ein Fehler entsteht, so gibt
readLine
eine null
-Referenz zurück. Wir schließen
daraus, dass das Dateiende erreicht ist und lösen eine
EOFException
aus.
Um den gesamten Inhalt einer Datei zu lesen, formulieren wir eine
Endlosschleife, die erst bei Erreichen des Dateiendes durch eine
EOFException
beendet wird:
public class AdressProgramm { public static void main(String argv[]) { AdressKartei kartei = new AdressKartei(); Adresse adr; try { DataInputStream eingabeStrom = new DataInputStream( new FileInputStream("textdatei.txt")); try { while (true) { adr = new Adresse(); adr.einlesen(eingabeStrom); kartei.einfügen(adr); } } catch (EOFException e) { } finally { eingabeStrom.close(); } } catch (IOException e) { System.out.println("Dateifehler in "+e.getMessage()); } kartei.ausgeben(); } }Wir erzeugen zunächst einen
DataInputStream
, der seine
Eingaben aus einem FileInputStream
bezieht. Dieser wiederum
liest Daten aus der Datei textfile.txt
.
Im nächsten try
-Block steht unsere Endlosschleife. Hier
erzeugen wir immer wieder ein neues Adressobjekt, lesen mit der
Methode einlesen
die Felder aus dem erzeugten
Stream und fügen die neue Adresse in die Kartei ein.
Wenn jetzt während der Endlosschleife das Dateiende erreicht wird,
so bricht diese mit einer EOFException
ab. Deshalb
behandeln wir jetzt diese Ausnahme mit einem catch
-Block,
der keine Anweisungen enthält. Jede andere Ausnahme
wird an den nächsthöheren try
-Block weitergereicht.
In ihm fangen wir alle Arten von Ein-/Ausgabefehlern ab, die ja
mit Unterklassen von IOException
gemeldet werden.
Analog dazu können wir auch alle Adressdaten in eine Textdatei schreiben,
in dem wir einen PrintStream
zur Ausgabe benutzen.
PrintStream
bietet dazu die Methoden print
und
println
. Diese kennen wir schon von der Standardausgabe
System.out
, denn dies ist nichts anderes als ein
PrintStream
.
Wir wollen aber ein etwas kompakteres Format erreichen, mit dem wir auch
das Alter ohne große Umwandlungsprobleme schreiben und lesen können.
Dazu errinnern wir uns an den DataInputStream
und sein
Pendant DataOutputStream
. Dieses Paar erlaubt das Lesen und
Schreiben aller Grunddatentypen in einem genormten Format.
// in Klasse Adresse einfügen void einlesenAusDatei(DataInputStream datei) throws IOException { name = datei.readLine(); vorname = datei.readLine(); alter = datei.readInt(); } void schreibenInDatei(DataOutputStream datei) throws IOException { datei.writeChars(name); datei.writeChar('\n'); datei.writeChars(vorname); datei.writeChar('\n'); datei.writeInt(alter); }Für die Ausgabe der Daten in den
String
s müssen wir
einen kleinen Kunstgriff ansetzen, da DataOutputStream
dafür keine Methode zum Schreiben vorsieht. Wir schreiben unsere
String
s mit writeChars
, die sie als Zeichenfolge
in die Datei schreibt. Anschließend beenden wir die Ausgabe mit
dem CR
-Zeichen, dass einen Zeilenumbruch darstellt. In
Java werden einzelne Zeichen mit den einfachen Anführungsstrichen
angegeben. \n
wird in das Zeichen CR
übersetzt.
Beim Einlesen können wir dann wieder mit der einfachen Methode
readLine
arbeiten.
Wir können jetzt die ganzen Daten im Vektor in eine Datei schreiben.
Dazu erhält die Klasse AdressKartei
eine neue Methode:
// in Klasse AdressKartei einfügen void speichern(String datName) throws ArrayIndexOutOfBoundsException, IOException { DataOutputStream os = new DataOutputStream( new FileOutputStream(datName)); for (int i = 0; i < adrVector.size(); i++) { Object obj = adrVector.elementAt(i); if (obj instanceof Adresse) { ((Adresse)obj).schreibenInDatei(os); }; } os.close(); }Genauso könnt ihr jetzt eine Methode für die Klasse
AdressKartei
schreiben, die die Dateidatei wieder
ausliest. Im Hauptprogramm
könnt ihr dann eine Datei öffnen, ein paar neue Daten
eingeben und alle wieder speichern. Wir wünschen euch
viel Spaß beim rumexperimentieren, bis zum nächsten Mal.