Von Enno Runne und Hendrik C.R. Lock
Die Ein- und Ausgabemöglichkeiten in Java werden
durch die reichhaltige Standardbibliothek java.io
festgelegt. Ein- und Ausgabe sind im Gegensatz zu C
typsicher, d.h. das Typsystem überprüft,
ob die richtigen Operationen auf den richtigen Daten ausgeführt werden.
Außerdem werden Fehlersituationen durch ein Reihe fein abgestimmter
Ausnahmeklassen behandelt (siehe Internet Online Januar 1997).
Die Ein- und die Ausgabe wird in Java durch Ströme dargestellt, in denen die Daten in fester Reihenfolge zuerst abgelegt und danach abgeholt werden. Ein solcher Strom bindet eine Datei, die Konsole oder einen Puffer im Hauptspeicher an. Ein einfacher Strom realisert z.B. eine byte-weise Zeichenübertragung.
Das folgende Programmbeispiel liest Zeichen von der Konsole ein und gibt sie gleich wieder aus.
import java.io.*; public class Echo1 { public static void main(String[] args) throws IOException { System.out.println( "ein: " + System.in.getClass() ); System.out.println( "aus: " + System.out.getClass() ); // lese von der Standardeingabe ein byte eingabe[] = new byte[128]; System.in.read(eingabe); // schreibe auf Standardausgabe System.out.println(eingabe); } }Zu Beginn binden wir die Bibliothek
java.io
komplett ein. Alle Klassen, die zur Ein- und Ausgabe
benötigt werden, sind nun bekannt.
Dadurch werden auch die Objekte
System.in
und System.out
angelegt, über
die die E/A-Operationen read(byte[])
und print(String)
aufgerufen werden.
Weil die Lesefunktion read()
eine Ausnahme auslösen kann,
wird dies bei der Hauptfunktion angegeben.
Die Ausführung ergibt folgendes:
> ein class java.io.BufferedInputStream > aus class java.io.PrintStream > hallo welt > [B@ee300940
System.in
ist also ein Strom der Klasse BufferedInputStream
,
und
System.out
ein Strom der Klasse PrintStream
.
Die Operation read()
liest bis zum Zeilenende ein,
d.h. "hallo welt". Aber
zu unserer Überraschung ist die Ausgabe "[B@ee300940".
Das liegt daran, dass read(byte[])
eine ziemlich primitive
Opration ist, die nur ein Feld von Bytes einliest, wohingegen
System.out.println()
auf einem aus Zeichen bestehenden Feld arbeitet.
Der Effekt wird also durch einen Darstellungsunterschied
zwischen Bytes und Zeichen verursacht. Die Konvertierung
des Bytefeldes in eine Zeichenkette behebt das Problem schlagartig.
String kette = new String(eingabe,0); System.out.println(kette);
Wie das Beispiel zeigt, haben Ströme bestimmte Eigenschaften bezüglich der Darstellung und Verarbeitung ihrer Daten. Dadurch erlauben die vorgegebenen Stromtypen E/A auf unterschiedlichen Niveaus. Die zentrale Idee der E/A Programmierung in Java ist es, Ströme aus mehreren Niveaus je nach Bedarf zusammenschalten, um vom Format der E/A zum benötigten Format zu gelangen (siehe Schaubild 1). Einige wichtige Ströme werden im Folgenden vorgestellt.
Die einfachsten Ströme gehören zur
Klasse InputStream
, sie
definiert Basismethoden für primitive byte-weise E/A.
Deren wichtigste Methode ist int read(byte b[])
,
die an alle Eingabeströme vererbt wird.
Die Unterklassen (siehe Schaubild 2)
definieren Methoden mit spezielleren Eigenschaften.
So wird z.B. ein FileInputStream
zum Lesen aus Dateien verwendet.
Analog gibt es zu jeder xxxInputStream
-Klasse eine
xxxOutputStream
-Klasse mit denselben Eigenschaften.
Die wichtigsten Ströme für die
Kommunikation mit der Konsole, die wir ja bereits mehrfach verwendet haben,
sind in Tabelle 1 zusammengefasst.
Die Operation println(String)
verhält sich dabei
wie print(String)
, außer dass zusätzlich ein Zeilenende
ausgegeben wird.
Tabelle 2 fasst Dateioperationen zusammen.
Standardeingabe
Standardausgabe Standardfehlerausgabe |
System.in
System.out
System.err
|
BufferedInputStream
PrintStream
PrintStream
|
int read(byte[])
print(String)
print(String)
|
Dateieingabe
Dateiausgabe |
FileInputStream
FileOutputStream
|
int read(byte[])
write(byte[],int,int)
|
Schaubild 1 zeigt, wie
einfachere Ströme in Ströme mit
komplexeren Eigenschaften eingespeist werden.
So erkennt z.B. die Klasse DataInputStream
das Zeilenende in
Eingaben und kann so ganze Zeilen als String
an
das Programm zurückliefern.
Wir nutzen dies im folgenden Beispiel, um gepuffert von einer Datei zu lesen
und die Daten zeilenweise auszulesen.
FileInputStream fin = new FileInputStream("test.dat"); BufferedInputStream bin = new BufferedInputStream(fin, 1024); DataInputStream din = new DataInputStream(bin); String zeile = din.readLine();In diesem Programmfragment werden Ströme per Konstruktor geöffnet und dann als Argument dem nächsten Stromkonstruktor übergeben. Durch diese Form der Aneinanderreihung von Strömen ist es also möglich, die Funktionalität verschiedener Stromklassen zusammenzuführen. Insbesondere haben wir durch die Puffererung den Einlesevorgang beschleunigt, die verwendete Puffergröße
1024
im Konstruktor ist dabei optional.
String
ein, was wir über einen
DataInputStream
organisieren.
import java.io.*; //... diese Methode 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(); }Unsere Klasse
Adresse
erweitern
wir um die Methode einlesen()
. Zunächst geben wir
mit throws
an, dass alle Ausnahmen, die durch fehlerhafte
Eingaben entstehen,
an die aufrufende Methode weitergeleitet werden sollen.
An dieser Stelle lässt sich später eine Fehlerbehandlung
einfügen.
Systemseitig geschieht nun das Folgende:
alle Eingaben der Tastatur werden
an das Objekt eingabeStrom
der Klasse DataInputStream
weitergegeben, das sie aufsammelt.
Eine komplette Eingabezeile wird dann mir der Methode readLine()
abgerufen. Weitere Aufrufe geben dann jeweils die nächste Eingabezeile zurück.
Hierbei ist bemerkenswert,
dass DataInputStream
maschinenunabhängig arbeitet:
bei DOS-Systemen wird CR
und LF
erwartet,
während unter Unix das Zeichen CR
ausreicht.
Die neue Methode liest allerdings noch kein Alter ein.
Hier treffen wir zunächst auf eine technische Hürde.
Wenn wir uns die Methoden von DataInputStream
ansehen, finden wir dort eine Methode
public final int readInt()Sie liefert uns scheinbar einen eingelesenen
int
-Wert.
Weil aber DataInputStream
auch zum Lesen von
Dateien in einem systemunabhängigen Format dient, erwartet
diese Methode eine Darstellung des Wertes durch genau vier Zeichen,
aus denen dann der int
-Wert
berechnet wird. Textuelle Ziffernfolgen besitzen aber eine andere Darstellung,
und somit würden wir nicht das gewünschte Ergebnis erhalten.
Wir kommen aber über einen Umweg ans Ziel: Wir lesen das Alter
ebenfalls als String
und verwandeln anschließend die
eingegebenen Zeichen in einen Wert. Dazu bietet die Klasse Integer
die Methode static int parseInt(String)
an.
Integer
ist eine Klasse, die den Basistyp int
auf die Objektebene hievt (denn numerische Werte sind keine Objekte).
System.out.println("Alter: "); String stringAlter = eingabeStrom.readLine(); setzeAlter(Integer.parseInt(stringAlter));Nach dem Einlesen einer Zeile wird die dazugehörige Zeichenkette in einen numerischen Wert umgewandelt. Der Aufruf der Methode
parseInt()
wird hier direkt mit dem Klassennamen Integer
qualifiziert,
da es sich um eine statisch gebundene
Methode handelt, d.h. um eine einzige Methode,
die alle Objekte dieser Klasse gemeinsam benutzen.
Jetzt kann es aber geschehen, dass ein Benutzer eine
Eingabe macht, die keine Zahl darstellt. In diesem Fall löst
parseInt
eine NumberFormatException
aus.
Weiterhin könnte unsere Methode setzeAlter
die Ausnahme InvalidAgeException
veranlassen.
Wir modifizieren nun die Einlesemethode so,
dass ein Benutzer bei fehlerhaften Eingaben die Eingabe wiederholt.
Die vollständige Methode sieht dann so aus:
class Adresse { // ... 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(); try { setzeAlter(Integer.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); } }Die
do-while
-Schleife wird mindestens
einmal ausgeführt und solange wiederholt, bis fehler==false
.
Mittels den try
-catch
-Konstruktionen erhalten wir eine
rudimentäre Fehlerbehandlung.
Natürlich können bei der Eingabe auch
andere Ausnahmen entstehen, die allesamt Spezialfälle der
IOException
sind. Solche Ausnahmen behandeln wir in der
Methode einlesen
nicht, sondern geben sie mit throws
weiter.
Ein Beispiel war das gepufferte Einlesen von Zeilen aus einer Datei,
das wir durch Verkettung von drei Strömen erreichten.
Weiterhin wurde gezeigt, wie sich eine Ziffernfolge in einen
numerischen (int
) Wert umwandeln lässt.
Noch sind die eingelesen Adressdaten flüchtig, aber in der nächsten Ausgabe werden sie auf Dateien gebannt. Bis dahin wünschen wir euch viel Spaß mit Java.