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.