Previous Page TOC Index Next Page See Page

Streams und I/O

Heute lernen Sie Java-Streams und Dateien kennen. Hier werden die Unterschiede zwischen Ein- und Ausgabestreams aufgezeigt. Unter anderem werden Sie die folgenden Dinge lernen:


Keines der heute gezeigten Beispiele funktioneirt, wenn Sie nicht die folgende Zeile oben in Ihrer Quellcode-Datei einfügen:

import java.io.*

Wir beginnen mit einer kleinen Geschichte zur Einführung von Streams und ihren Vorgängern, Pipes.


Eine Pipe ist ein Mechanismus, um Daten von einem Element in einem System an ein anderes weiterzugeben. Das Element, das die Information durch die Pipe sendet, ist die Quelle, das Element, das sie empfängt ist das Ziel.


Ein Stream ist ein Kommunikationspfad zwischen einer Informationsquelle und einem Ziel. Das Element, das die Information durch den Stream sendet, ist der Erzeuger, das Element, das die Information empfängt, ist der Verbraucher.


Ein Prozessor ist ein Filter, der Daten irgendwie manipuliert, während sie ihn durchlaufen - d.h. zwischen Erzeuger und Verbraucher.


Eine der ersten Erfindungen des UNIX-Betriebssystems war die Pipe. Durch diese Vereinheitlichung der Kommunikation in einer Metapher hat UNIX den Weg für zahlreiche ähnlicher Mechanismen geebnte, unter anderm auch für die Abstraktion der Streams.

Ein nicht interpretierter Byte-Stream kann aus einer beliebigen Pipe-Quelle stammen, wobei es sich um Dateien, Programme, den Speicher des Computers oder sogar das Internet handeln kann. Die Quelle und das Ziel eines Streams sind völlig belibige Erzeuger und Verbraucher von Bytes. Und hier liegt die Stärke der Abstraktion. Sie müssen nichts über die Quelle der Information wissen, wenn Sie aus einem Stream lesen, und Sie müssen nichts über das Ziel wissen, wenn Sie in einen Stream schreiben.

Allgemeine Methoden, die von jeder Quelle lesen können, nehmen ein Stream-Argument entgegen, das die Quelle darstellt; allgemeine Methoden zum Schreiben nehmen einen Stream entgegen, der das Ziel darstellt. Prozessoren verwenden zwei Stream-Argumente. Sie lesen aus dem ersten Argument, verarbeiten die Daten und schreiben die Ergebnisse in das zweite Argument. Diese Prozessoren wissen nichts über die Quelle oder das Ziel der Daten. Quelle und Ziel können variieren - von einem Speicherpuffer auf dem lokalen Computer bis hin zu den Testdateien der NASA.

Die Loslösung des Erzeugens, Verarbeitens und Verbrauchens der Daten von den Quellen und Zielen dieser Daten ermöglicht Ihnen, in Ihrem Programm alle möglichen Kombinationen vorzunehmen. Wenn irgendwann neue Quellen und Ziele eingeführt werden, können sie in derselben Umgebung eingesetzt werden ohne daß sich Ihre Klassen ändern müssen. Darüber hinaus können neue Stream-Abstraktionen geschrieben werden, die höhere Interpretationsebenen unterstützten, und zwar völlig unabhängig von dem Transportmechanismus für die eigentlichen Datenbytes.

Die Grundlagen für diese Stream-Umgebung sind die beiden abstrakten Klassen InputStream und OutputStream. Wir beginnen mit diesen beiden Oberklassen und wandern dann in der Hierarchie nach unten.


Die Methoden, die Sie heute kennenlernen, werfen eine IOException auf. Diese Unterklasse von Exception verkörpert alle möglichen Eingabe/Ausgabe-Fehler, die bei der Verwendung von Streams auftreten könnten. Einige Unterklassen definieren spezifischere Ausnahmen, die ebenfalls aufgeworfen werden können. Hier müssen Sie nur wissen, daß Ihr Code entweder eine IOException auffangen oder die Ausnahme in der Hierarchie nach oben weiterreichen muß, um die Streams korrekt zu verwenden.

Eingabestreams

Eingabestreams lesen Daten aus verschiedenen Eingabequellen, wie etwa der Tastatur. Dabei wird jeweils ein Byte gelesen. Die von einem Eingabestream gelesenen Daten können auf unterschiedliche Weise an beliebige Verbraucher dieser Daten weitergegeben werden. In diesem Abschnitt werden die folgenden Eingabestream-Klassen beschrieben:

Die abstrakte Klasse InputStream

InputStream ist eine abstrakte Klasse, die definiert, wie ein Verbraucher einen Bytestream aus einer Quelle liest. Die Identität der Quelle und die Methode, wie die Bytes erzeugt und transportiert werden, sind irrelevant. Der Eingabstream ist einfach das Ziel dieser Bytes, das ist alles, was Ihr Programm wissen muß.

Alle Eingabestreams leiten sich von der abstrakten Klasse InputStream ab. Alle haben die hier beschriebenen Methoden gemeinsam. Der Stream s in den folgenden Beispielen kann also einer der komplexeren Streams sein, die später in diesem Abschnitt noch beschrieben werden. Die Methode read() und skip() stellen eine grundlegende Standardfunktionalität der abstrakten Klasse bereit. Die Methoden available(), close(), markSupported(), mark() und reset() sind einfach Gerüste und müssen in einer Stream-Unterklasse überschrieben werden, damit sie etwas sinnvolles erledigen.

read()

Die wichtigste Methode des Verbrauches eines Eingabestreams ist diejenige, die die Bytes aus der Quelle liest. Die read()-Methode liegt in mehreren Varianten vor, die jeweils blockieren, bis die angeforderten Eingaben zur Verfügung stehen.


Machen Sie sich keine Sorgen über diese Einschränkung; durch das Multithreading kann das Programm beliebige Aktionen vornehmen, während es auf die Eingaben wartet. Häufig weist man jedem Eingabe/Ausgabestream einen Thread zu, der nur für das Lesen/Schreiben der Daten zuständig ist. Diese Threads reichen die Information an andere Threads weiter, die sie verarbeiten, so daß sich I/O-Zeit und Rechenzeit Ihres Programms überlappen. Hier vergessen wir das jedoch für einen Moment und tun so, als hätten wir es nur mit Eingaben und Ausgaben zu tun.

Die erste Form von read() liest einfach nur ein einzelnes Datenbyte:

InputStream s = getAnInputStream ();
System.out.println("Bytes read: " + s.read());

Wenn read() erfolgreich ist, gibt es 1 zurück (einen Integer, der die Anzahl der gelesenen Bytes angibt), andernfalls -1. Daran erkennen Sie, daß Sie das Ende des Eingabestreams erreicht haben, oder daß im Stream keine Bytes enthalten waren.

Hier folgt ein Beispiel für die zweite Form der read()-Methode, die einen Puffernamen als einziges Argument entgegennimmt:

byte[] myBuff = new byte[1024]; // eine belibige Größe ist OK
System.out.println("Bytes read: " + s.read(myBuff));

Diese Form von read() versucht, den ganzen Puffer zu füllen, die ihr übergeben wird. Wenn das nicht möglich ist, gibt sie die Anzahl der Bytes zurück, die in den Puffer eingelesen wurden. Jeder weitere Aufruf von read() gibt -1 zurück, d.h. Sie haben das Ende des Eingabestreams erreicht oder es sind keine Bytes im Eingabestream enthalten.

Weil es sich bei einem Puffer um ein Byte-Array handelt, können Sie einen Offset spezifizieren, ebenso wie die Anzahl der Bytes:

s.read(myBuff, 101, 300);

Dieses Beispiel versucht, zwischen 101 und 400 Bytes einzulesen und verhält sich sonst genau wie die zweite read()-Methode. Die Standardimplementierung dieser read()-Methode sieht genau so aus, wobei 0 als Offset und b.length als Anzahl der zu lesenden Bytes angegeben werden.

skip()

Was tun Sie, wenn Sie einige der Bytes im Stream überspringen oder das Lesen nicht am Anfang des Streams beginnen wollen? Die Methode skip() ist ähnlich der Methode read() und erledigt genau das für Sie.

if (s.skip(1024) != 1024)
System.out.println("Ich habe weniger Bytes übersprungen als vorgegeben.");

Damit würden Sie die nächsten 1024 Bytes im Eingabestream überspringen, und wenn die skip()-Methode nicht 1024 als die Anzahl übersprungener Bytes zurückgibt, wird diese Meldung ausgegeben. Die skip()-Methode nimmt einen long-Wert entgegen und gibt einen solchen zurück, weil Streams nicht auf eine bestimmte Größe beschränkt sind. Die Implementierung von skip() verwendet einfach read(), ohne die Byts irgendwo abzulegen; es wirft sie einfach in den Bit-Abfallkorb.

available()

Wenn Sie aus irgendeinem Grund wissen müssen, wie viele Bytes sich im Stream befinden, fragen Sie nach:

if (s.available() < 1024)
System.out.println("Es sind noch zu wenig Bytes enthalten.");

Damit ermitteln Sie die Anzahl der Bytes, die Sie ohne Blockieren lesen können. Weil die Quelle dieser Bytes abstrakt ist, ist nicht garantiert, daß Streams eine zuverlässige Antwort auf eine solche direkte Frage geben. Einige Streams geben beispielsweise immer 0 zurück. Wenn Sie keine speziellen Unterklassen von InputStream verwedenden, von denen Sie eine zuverlässige Antwort auf diese Frage erwarten können, sollten Sie sich nicht auf diese Methode verlassen. Durch das Multithreading werden viele der Probleme gelöst, die durch das Blockieren entstehen, während auf das Füllen des Streams gewartet wird. Einer der Gründe für die Verwendung von available() wird damit wesentlich weniger wichtig.

In InputStream gibt die Methode available() immer 0 zurück. Um etwas Sinnvolles zurückzugeben, müssen Sie sie in einer Unterklasse überschreiben.

markSupported(), mark() and reset()

Einige Streams erlauben, eine Position innerhalb des Streams zu markieren, einige Bytes zu lesen und den Stream dann wieder an die markierte Position zurückzusetezn, so daß die Bytes erneut eingelesen werden können. Der Stream müßte sich dann alle diese Bytes »merken«, so daß es Einschränkungen gibt, wie weit die Positionierung erfolgen kann, damit noch ein Zurücksetzen möglich ist. Es gibt eine Methode, die feststellt, ob dieses Konzept überhaupt unterstützt wird. Hier ein Beispiel:

InputStream s = getAnInputStream();
if (s.markSupported()) { // erlaubt der Stream eine Markierung
// und ein Zurücksetzen?
... // im Stream lesen
s.mark(1024);
... // maximal 1024 weitere Bytes lesen
s.reset();
... // jetzt können wir die Bytes erneut lesen
}
else {...} // nicht unterstützt - > etwas anderes tun

Die Methode markSupported() prüft, ob dieser Stream die Methoden mark() und reset() unterstützt. Weil es sich bei InputStream um eine abstrakte Klasse handelt, gibt markSupported() false zurück. Sie muß in der Stream-Unterklasse überschrieben werden, damit sie true zurückgibt und somit die Unterstüztung zusagt.

Die Methode mark() nimmt ein Argument entgegen, das angibt, wie viele Bytes Sie lesen können, bevor ein Zurücksetzen erfolgen muß. Wenn das Programm weiter liest als angegeben, wird die Markierung ungültig gemacht, und der Aufruf von reset() wirft eine Ausnahme auf. Andernfalls positioniert reset() den Stream auf die zuvor markierte Stelle. Diese Methoden sind nur Gerüste in InputStream; sie müssen in der Unterklasse definiert und überschriebenw erden. reset() kann eine Ausnahme aufwerfen, wenn Sie es direkt aus InputStream aufrufen.


Ein Beispiel für eine Unterklasse, in der markSupported() true ist und wo mark() und reset() definiert sind, finden Sie im Quellcode von java.io.BufferedInputStream.java. Am einfachsten wählen Sie dazu Datei | Öffnen/Erzeugen aus dem JBuilder-Menü, klicken auf die Registerkarte Java, wählen java.io und klicken auf OK.

Das Markieren und Zurücksetzen eines Streams ist vor allem sinnvoll, wenn Sie versuchen, den Datentyp zu identifizieren, der im Stream vorliegt, aber um das wirklich zu erkennen, müssen Sie ein Stück vorauslesen. Das ist häufig der Fall, wenn Sie mehrere Parser haben, denen Sie den Stream übergeben können, die aber eine (für Sie unbekannte) Anzahl an Bytes brauchen, bevor sie feststellen können, ob der Stream den für sie geeigneten Typ hat. Wählen Sie einen großen Wert für die Beschränkung zum Vorauslesen und führen Sie die Parser aus, bis diese entweder einen Fehler erzeugen oder erfolgreich ausgeführt werden. Wenn ein Fehler aufgeworfen wird, rufen Sie reset() auf und probieren den nächsten Parser.

close()

Weil Sie nicht unbedingt wissen, welche Ressource ein geöffneter Stream enthält, und damit auch nicht, wie Sie diese nach dem Lesen des Streams korrekt verarbeiten können, sollten Sie einen Stream explizit schließen, so daß er diese Ressourcen freigeben kann. Die Speicherbereinigung und eine finalization()-Methode können dies für Sie übernehmen, aber was wäre, wenn Sie diesen Stream neu öffnen wollen, bevor die Ressourcen durch diesen asynchronen Prozeß freigegeben wurden? Bestenfalls ist das müßig oder verwirrend, im schlimmsten Fall entsteht jedoch ein unerwarteter, seltsamer und schwer zu lokalisierender Fehler. Weil Sie mit externen Ressourcen interagieren, ist es sicherer, sie explizit freizugeben, wenn Sie sie nicht mehr brauchen:

InputStream s = alwaysMakesANewInputStream();
if (s != null) {
try {
... // s nutzen, bis Sie fertig sind
}
finally {
s.close();
}
}

Die Verwendung des finally-Blocks stellt sicher, daß der Stream immer geschlossen wird. Um zu vermeiden, daß ein Stream geschlossen wird, der nicht geöffnet oder erfolgreich erzeugt wurde, wird in diesem Beispiel geprüft, ob s nicht null ist, bevor der try-Block ausgeführt wird.

In InputStream macht die close()-Methode nichts; wenn sie funktional sein soll, muß sie in einer Unterklasse überschriebenw erden.

ByteArrayInputStream

Das Gegenteil einiger der vorigen Beispiele wäre, einen Eingabestream aus einem Byte-Array zu erzeugen. Und genau das macht ByteArrayInputStream:

byte[] myBuff = new byte[1024];
fillWithUsefulData(myBuff);
InputStream s = new ByteArrayInputStream(myBuff);

Die Leser des neuen Streams s sehen einen 1024 Byte langen Stream mit den Bytes aus dem array myBuff. Auch dieser Klassenkonstruktor bietet eine Form, die einen Offset und eine Länge berücksichtigt:

InputStream s = new ByteArrayInputStream(myBuff, 101, 300);

Hier ist der Stream 300 Byte lang und besteht aus den Bytes 101 bis 400 aus dem Array myBuff.


Sie heben hier gesehen, wie ein Stream erezugt wird. Diese neuen Streams sind mit den einfachsten aller möglichen Datenquellen verbunden, enem Array mit Daten aus dem Speicher des lokalen Computers.

ByteArrayInputStream implementiert einfach die Standardmethoden, die alle Eingabestreams implementieren. Hier hat die Methode available() eine sehr einfache Aufgabe - sie gibt 1024 bzw. 300 für die beiden Instanzen von ByteArrayInputStream zurück, die Sie zuvor erzeugt haben, weil es genau weiß, wie viele Bytes der Definition gemäß zur Verfügung stehen. Der Aufruf von reset() für einen ByteArrayInputStream setzt die Position auf den Anfang des Streams myBuff zurück, wenn keine Markierung vorgenommen wurde (statt eine Ausnahme aufzuwerfen). Und außerdem kann es springen wie ein Weltmeister - skip()!

FileInputStream

Einer der gebräuchlichsten Verwendungszwecke von Streams ist, sie Dateien im Dateisystem zuzuordnen.


Das Lesen und/oder Schreiben von Dateien ist zwar für Standalone-Java-Anwendungen kein problem, aber wenn Sie versuchen, von einem Applet aus auf Streams zuzugreifen, die auf Dateien basieren, kann das zu Sicherheitsproblemen führen (abhängig von der Sicherheitsebene, die der Anwender eingestellt hat). Wenn Sie Applets erzeugen, gehen Sie nicht von Dateien aus, sondern versuchen Sie statt dessen, Server zu verwenden, die gemeinsam genutzte Informationen bereitstellen.

Hier wird beispielsweise ein solcher Eingabestream auf einem UNIX-System erzeugt:

InputStream s = new FileInputStream("/some/path/and/filename");

Sie können den Stream auch aus einem zuvor geöffneten Dateideskriptor erzeugen:

int fd = openInputFile();
InputStream s = new FileInputStream(fd);

Weil dieser Streamtyp jedoch auf einer echten Datei (mit endlicher Länge) basiert, kann FileInputStream available() genau implementieren, ebenso wie skip(). Darüber hinaus kennt FileInputStream noch ein paar weitere Tricks:

FileInputStream aFIS = new FileInputStream("aFileName");
int myFD = aFIS.getFD();
/* close() wird von der Speicherbereinigung automatisch aufgerufen */


Sie müssen die Stream-Variable aFIS vom Typ FileInputStream deklarieren, weil InputStream diese neuen Methoden nicht kennt.

Der erste Teil, getFD(), gibt den Dateideskriptor der Datei zurück, auf der der Stream basiert. Die zweite Neuerung ist, daß Sie close() oder finalize() nicht direkt aufrufen müssen. Die Speicherbereinigung ruft close() automatisch auf, wenn sie erkennt, daß der Stream nicht mehr benötigt wird, aber noch vor dem eigentlichen Zerstören des Streams. Siekönnen also fröhlich im Stream weiterlesen und müssen ihn nie explizit schließen, und alles ist in Ordnung.

Das reicht aus, weil Streams, die auf Dateien basieren, nur wenig Ressourcen verbrauchen, und diese Ressourcen können nicht versehentlich wiederverwendet werden, bevor die Speicherbereinigung stattfindet. (Das waren die Themen, die in früheren Diskussionen von finalize() und close() angesprochen wurden). Wenn Sie jedoch auch in die Datei schreiben, müßten Sie sorgfältiger sein. Nur weil Sie den Stream nicht schließen müssen, heißt das nicht, daß nich tkönnen. Der Klarheit halber, oder wenn Sie nicht genau wissen, welchen Typ InputStream Ihnen vorlag, könnten Sie close() selbst aufrufen und den Stream somit in einem bekannten Status hinterlassen.

FilterInputStream

Diese Klasse stellt einfach nur einen Durchgang für alle Standardmethoden von InputStream dar. Sie reicht einen Stream in der Kette der Filter weiter, an die sie alle Methodenaufrufe weitergibt. Sie implementiert nichts neues, kann aber verschachtelt werden:

InputStream s = getAnInputStream();
FilterInputStream s1 = new FilterInputStream(s);
FilterInputStream s2 = new FilterInputStream(s1);
FilterInputStream s3 = new FilterInputStream(s2);
... s3.read() ...

Immer wenn ein read() für den gefilterten Stream s3 ausgeführt wird, gibt sie die Anforderung an s2 weiter, s2 macht dasselbe für s1 und schließlich wird s aufgefordert, die Bytes bereitzustellen. Unterklassen von FilterInputStream sollten natürlich die Bytes, die ihnen übergeben werden, sinnvoll verarbeiten. Die relativ auführliche Form der Weitergabe im vorigen Beispiel kann eleganter gemacht werden, weil dieser Stil genau die Verschachtelung verketteter Filder ausdrückt:

s3 = new FilterInputStream
(new FilterInputStream
(new FilterInputStream(s)));

Diese Klasse macht zwar selbst nicht viel, aber sie ist public deklariert, und nicht abstract. Das bedeutet, sie zwar relativ nutzlos ist, aber direkt instantiiert werden kann. Um jedoch etwas sinnvolles zu tun, sollten Sie eine der Unterklassen verwenden, die in den folgenden Abschnitten vorgestellt werden.

BufferedInputStream

Dies ist einer der wichtigsten aller Streams. Er implementiert alle InputStream-Methodeen, verwendet aber ein gepuffertes Byte-Array, das als Cache für zukünftige Leseoperationen dient. Damit sind Sie nicht mehr von der Größe der Abschnitte abhängig, die Sie aus den Streams lesen. Außerdem können intelligente Streams damit vorauslesen, wenn sie erwarten, daß Sie bald weitere Daten brauchen werden.

Weil die Pufferung von BufferedInputStream so praktisch ist und es sich außerdem um die einzige Unterklasse handelt, die mark() und reset() vollständig implemenitert, wollen Sie vielleicht, daß jeder Eingabestream diese Möglichkeiten nutzen kann. Normalerweise hätten Sie Pech, weil sie nicht von dieser Stream-Unterklasse abgeleitet werden. Aber Sie haben bereits eine Möglichkeit kennengelernt, wie Filter-Streams sich um andere Streams herum legen können. ms. Hier folgt eine gepufferte Version von FileInputStream, die das Markieren und Zurücksetzen korrekt vornimmt:

InputStream s = new BufferedInputStream(new FileInputStream("myfile"));

Damit haben Sie einen gepufferten Eingabestream, der auf myfile basiert, und der mark() und reset() verwenden kann. Jetzt fangen Sie an, die Leistungsfähigkeit verschachtelter Streams zu verstehen. Die von einem Filter-Eingabestream bereitgestellte Funktionalität kann durch die Verschachtelung von jedem anderen Stream genutzt werden.

DataInputStream und DataInput

Alle Methoden der Klasse DataInputStream überschreiben die abstrakten Methoden aus der DataInput-Schnittstelle. Diese Schnittstelle ist allgemein genug gehalten, daß Sie sie in Ihren eigenen Klassen verwenden können.

Wenn Sie anfangen, mit Streams zu arbeiten, werden Sie schnell feststellen, daß diese kein strenges Format aufweisen, in das sie alle Daten zwigen. Insbesondere die elementaren Typn von Java stellen eine praktische Möglichkeit dar, Daten darzustellen, aber mit den Streams, die Sie bisher definiert haben, konnten Sie keine Daten dieses Typs lesen. Die DataInput-Schnittstelle definiert höhere Methoden, die einen komplexeren, typisierten Datenstream zum Lesen und zum Schreiben unterstützen. Hier die Methodensignatur der Schnittstelle:

void readFully(byte buf[]) throws IOException;
void readFully(byte buf[], int off, int len) throws IOException;
int skipBytes(int n) throws IOException;
boolean readBoolean() throws IOException;
byte readByte() throws IOException;
int readUnsignedByte() throws IOException;
short readShort() throws IOException;
int readUnsignedShort() throws IOException;
char readChar() throws IOException;
int readInt() throws IOException;
long readLong() throws IOException;
float readFloat() throws IOException;
double readDouble() throws IOException;
String readLine() throws IOException;
String readUTF() throws IOException;

Dier ersten drei Methoden stellen einfach nur neue Formen von read() und skip() dar, die Sie schon kennen. Die nächsten 10 Methoden lesen entweder einen elementaren Typ oder sein vorzeichenloses Gegenstück ein. Diese 10 Methoden müssen einen größeren Integer zurückgeben, weil Integer in Java immer vorzeichenbehaftet sind, so daß auch der vorzeichenlose Wert nicht in einen kleineren Integer paßt. Die beiden letzten Methoden lesen einen durch ein Neue-Zeile-Zeichen terminierten Zeichenstring (der mit \r, \n oder \r\n endet) aus dem Stream ein. Die Methode readLine() liest ASCII-Zeichen, die Methode readUTF() liest Unicoce.

DataInputStream implementiert die DataInput-Schnittstelle - d.h. DataInputStream bietet konkrete Definitionen für die abstrakten Methoden von DataInput. Nachdem Sie wissen, wie die von DataInputStream definierte Schnittstelle aussieht, wollen wir sie in der Praxis betrachten. In diesem Beispiel ist das erste Element im Stream ein long-Wert, der die Größe des Streams enthält:

DataInputStream s = new DataInputStream(getNumericInputStream());
long size = s.readLong(); // die Anzahl der Elemente im Stream
while (size-- > 0) {
if (s.readBoolean()) { // soll ich dieses Element verarbeiten?
int anInteger = s.readInt();
int magicBitFlags = s.readUnsignedShort();
double aDouble = s.readDouble();
if ((magicBitFlags & 010000) != 0) {
... // High-Bit gesetzt - > Verarbeitung
}
... // anInteger und aDouble verarbeiten
}
}

Weil die Klasse eine Schnittstelle für alle ihre Methoden definiert, können Sie auch schreiben:

DataInput d = new DataInputStream(new FileInputStream("anyfile"));
String line;
while ((line = d.readLine()) != null) {
... // Zeile verarbeiten
}

Noch eine letzte Anmerkung zu den meisten der Methoden von DataInputStream: Wenn das Streamende erreicht ist, werfen sie eine EOFException auf. Das ist ganz praktisch, weil Sie diese auffangen und die entsprechenden Maßnahmen ergreifen können:

try {
while (true) {
byte b = (byte) s.readByte();
... // process the byte b
}
}
catch (EOFException e) { // Streamende erreicht
... // entsprechende Maßnahmen
}

Das funktionert für alle Methoden in dieser Klasse bis auf drei: skipBytes(), readLine() und readUTF(). Die Methode skipBytes() macht nichts, wenn sie das Streamende erreicht. Die Methode readLine() gibt null zurück, wenn sie das Ende erreicht. Wenn die Methode readUTF() überhaupt feststellt, daß irgendein Problem besteht, wirft sie eine UTFDataFormatException auf.

PushbackInputStream

Der Filterstream PushbackInputStream ist sehr praktisch zum Zurücklesen von Daten, indem sie auf den Stream zurückgegeben werden, von dem sie kamen, was häufig in Parsern verwendet wird. Sie können einen Ein-Byte-Pushbackpuffer verwenden oder die Größe des Pushback-Puffers angeben. Neben den read()-Methoden stellt diese Unterklasse auch drei unread()-Methoden bereit und verwendet eine vereinfachte Version zum Markieren und Zurücksetzen. Listing 10.1 ist eine einfache Implementierung von readLine(), das diese Klasse verwendet. Diese neue Klasse kann von anderen Klassen importiert werden, die die Funktionen dieser Implementierung nutzen wollen.

Listing 10.1: SimpleLineReader.java

1: import java.io.*;
2:
3: public class SimpleLineReader {
4: private FilterInputStream s;
5:
6: public SimpleLineReader(InputStream anIS) {
7: s = new DataInputStream(anIS);
8: }
9:
10: // ... other read() methods using stream s
11:
12: public String readLine() throws IOException {
13: char[] buffer = new char[100];
14: int offset = 0;
15: byte thisByte;
16:
17: try {
18: lp: while (offset < buffer.length) {
19: switch (thisByte = (byte) s.read()) {
20: case '\n':
21: break lp;
22: case '\r':
23: byte nextByte = (byte) s.read();
24: if (nextByte != '\n') {
25: if (!(s instanceof PushbackInputStream)) {
26: s = new PushbackInputStream(s);
27: }
28: ((PushbackInputStream)s).unread(nextByte);
29: }
30: break lp;
31: default:
32: buffer[offset++] = (char) thisByte;
33: break;
34: }
35: }
36: }
37:
38: catch (EOFException e) {
39: if (offset == 0)
40: return null;
41: }
42:
43: return String.copyValueOf(buffer, 0, offset);
44: }
45:
46: }


Dieser Code demonstriert verschiedene Dinge. Des Beispiels halber wurde readLine() auf die ersten 100 Zeichen der Zeile beschränkt (ishe Zeilen 12 und 13), statt eine Zeile beliebiger Länge zu lesen, wie Sie es in einer allgemeinen Anwendung handhaben würden. Außerdem zeigt es Ihnen noch einmal, wie man eine Schleife verläßt (Zeilen 18 bis 35) und wie aus einem Array mit Zeichen ein String erzeugt wird (Zeile 43). Dieses Beispiel verwendet außerdem die Standard-read()-Methode von InputStream (Zeile 19), um Bytes zu lesen und das Ende des Streams zu ermitteln, indem es sie in einen DataInputStream einbindet (Zeile 7) und eine EOFException auffängt (Zeilen 38 bis 41).


Einer der außergewöhnlichsten Aspekte des Beispiels ist, wie PushbackInputStream verwendet wird. Um sicherzugehen, daß ein \n, das einem \r folgt, ignoriert wird, müssen Sie ein Zeichen vorauslesen, aber wenn es sich nicht um \n handelt müssen Sie es zurückschreiben (Zeilen 22 bis 30). Betrachten Sie die Zeilen 25 und 26 so, als ob Sie nichts über den Stream s wüßten. Als erstes prüfen Sie, ob s eine Instanz von PushbackInputStream ist (Zeile 26). Wenn das der Fall ist, können Sie es einfach verwenden. Wenn nicht, schließen Sie den aktuellen Stream (was immer das auch ist) in einen neuen PushbackInputStream ein und verwenden statt dessen diesen (Zeile 26).

Anschließen wird die unread()-Methode aufgerufen (Zeile 28). Das stellt ein Problem dar, weil s zur Compile-Zeit den Typ FilterInputStream hat und damit diese Methode nicht erkennt. Die vorhergehenden Codezeilen (Zeilen 25 und 26) haben jedoch sichergestellt, daß s den Laufzeittyp PushbackInputStream hat, Sie können es also sicher in diesen Typumwandeln und dann problemlos unread() aufrufen.


Dieses Beispiel wurde der Demonstration halber eher ungewöhnlich angelegt. Sie hätten ganz einfach auch eine PushbackInputStream-Variable deklarieren und den DataInputStream immer darin einschließen können. Der Konstruktor von SimpleLineReader hätte einfach prüfen können, ob sein Argument bereits die richtige Klasse hat - so wie PushbackInputStream es gemacht hat -, bevor er einen neuen DataInputStream erzeugt. Das Interessante bei diesem Ansatz, eine Klasse nur bei Bedarf einzuhüllen, ist, daß es für jeden InputStream funktioniert, den Sie übergeben, und daß der Mehraufwand nur anfällt, wenn er wirklich erforderlich ist. Das ist eine sinnvolle Strategie.

Diese Klasse unterstützt auch die Methoden mark() und reset(); markSupported() ergibt true.

java.security.DigestInputStream_ class"

Diese Klasse ist zwar im Paket java.security implementiert, aber sie ist von FilterInputStream abgeleitet. Sie erzeugt die Eingbe, die für das java.security.MessageDigest-Objekt erforderlich ist, ein Byte-Array, und kann aktiviert oder deaktiviert werden. Wenn der Stream aktiviert ist, aktualisiert ein read() das Digest, wenn er deaktiviert ist, wird das Digest nicht aktualisiert. Der Konstruktor hat die folgende Form:

DigestInputStream(anInputStream, aMessageDigest)

dabei ist anInputStream ein InputStream oder eine Ableitung davon, und aMessageDigest ist der Digest, der von diesem Stream aktualisiert werden soll. Weitere Informationen über die Klasse MessageDigest finden Sie in der Java-API-Dokumentation zum Paket java.security.

java.util.zip.CheckedInputStream

Diese Klasse ist zwar im Paket java.util.zip implementiert, aber sie leitet sich von FilterInputStream ab. Sie hat die Aufgabe, einen Stream zu erzeugen, der auch eine Prüfsumme der gelesenen Daten verwalten kann. Der Konstruktor hat die folgende Form:

CheckedInputStream(anInputStream, aChecksum)

anInputStream ist eine InputStream-Klasse oder Unterklasse, und aChecksum ist CRC32 oder Adler32. Weitere Informationen über diese Prüfsummen finden Sie in der Java-API-Dokumentation für das Paket java.util.zip.

java.util.zip.InflaterInputStream

Diese Klasse ist zwar im Paket java.util.zip implementiert, aber sie leitet sich von FilterInputStream ab. Sie hat die Aufgabe, einen Stream zu erzeugen, Daten dekomprimieren kann, die komprimiert vorliegen. Der Konstruktor hat die folgende Form:

InflaterInputStream(anInputStream, anInflater)
InflaterInputStream(anInputStream, anInflater, theSize)

dabei ist anInputStream eine InputStream-Klasse oder -Unterklasse, und anInflator ist die zu verwendende Dekomprimierung. Der erste Konstruktor erzeugt einen Stream mit einer Standard-Puffergröße, der zweite ermöglicht Ihnen, die Größe im Argument theSize anzugeben. Außerdem hat diese Klasse zwei Unterklassen: java.util.zip.GZIPInputStream, die Daten liest, die im GZIP-Format komprimiert wurden, und java.util.zip.ZipInputStream, die Daten liest, die im ZIP-Dateiforamt komprimiert wurden (und sie implementiert java.util.zip.ZipConstants). Hier die Konstruktoren:

GZIPInputStream(anInputStream)
GZIPInputStream(anInputStream, theSize)
ZIPInputStream(anInputStream)

Wie Sie sehen, haben diese Konstruktoren keinen anInflater als Argument, weil sie für bestimmte Komprimierungsfomrmate spezifisch sind (die beiden ersten für GZIP, der letzte für ZIP). Außerdem hat ZIPInputStream keinen Konstruktor, der Ihnen ermöglicht, die Puffergröße anzugeben.

Weitere Informationen finden Sie in der Java-API-Dokumentation.

ObjectInputStream

ObjectInputStream implementiert die Schnittstellen java.io.ObjectInput und java.io.ObjectStreamConstants und wird zur Deserialisierung (Laden) elementarer Daten und Graphen von Objekten verwendet, die zuvor mit ObjectOutputStream gespeichert wurden. Diese beiden Klassen stellen Ihrer Anwendung eine Möglichkeit zur Verfügung, Objekte persistent zu speichern. Beispiele für die Arbeitsweise von ObjectInputStream und ObjectOutputStream finden Sie im Abschnitt über ObjectOutputStream.

PipedInputStream

PipedInputStream und die gleichrangige Klasse PipedOutputStream werden kombiniert, um eine einfache Zweiwege-Kommunikation zwischen Threads zu realisieren. Diese beiden Klassen werden im Abschnitt PipedOutputStream genauer vorgestellt.

SequenceInputStream

Angenommen, Sie haben zwei separate Streams und möchten sie zusammensetzen, so wie bei der Konkatenation von zwei Strings. Und genau dafür wurde SequenceInputStream entwickelt:

InputStream s1 = new FileInputStream("theFirstPart");
InputStream s2 = new FileInputStream("theRest");
InputStream s = new SequenceInputStream(s1, s2);
... s.read() ... // liest aus beiden Streams

Sie könnten die Dateien auch hintereinander lesen, aber einige Methoden erwarten einen einzigen InputStream, was mit SequenceInputStream ganz einfach zu realisieren ist. Aber was machen Sie, wenn Sie mehrere Streams zusammensetzen wollen? Sie könnten folgendes probieren:

Vector v = new Vector();
... // set up all the streams and add each to the Vector
/* now concatenate the vector elements into a single stream */
InputStream s1 = new SequenceInputStream(v.elementAt(0),
v.elementAt(1));
InputStream s2 = new SequenceInputStream(s1, v.elementAt(2));
InputStream s3 = new SequenceInputStream(s2, v.elementAt(3));
...


Ein Vector ist ein dynamisches Array mit Objekten, das gefüllt werden kann, auf das mit der Methode elementAt() zugegriffen werden kann, und desse Inhalt aufgelistet werden kann.

Es gibt jedoch noch eine Alternative, die einen anderen Konstruktor verwendet: _ class"

Vector v = new Vector();
... // Alle Streams einrichten und dem Vector hinzufügen
/* jetzt die Elemente des Vectors zu einem einzelnen Stream konkatenieren */
InputStream s = new SequenceInputStream(v.elements());
...

Dieser Konstruktor nimmt eine Auflistung aller Streams, die Sie konkatenieren wollen, entgegen und gibt einen einzelnen Stream zurück, der hintereinander alle Daten liest.

StringBufferInputStream


Lassen Sie sich vom Namen dieser Klasse nicht täuschen. StringBufferInputStream hätte besser StringInputStream heißen sollen, weil sie auf einem String-Objekt basiert, nicht auf einem StringBuffer-Objekt.

StringBufferInputStream ist funktional dasselbe wie ByteArrayInputStream, basiert aber nicht auf einem Byte-Array, sondern auf einem Zeichen-Array (einem String):

String aBuff = "Now is the time for all good ...";
InputStream s = new StringBufferInputStream(aBuff);

Weitere Informationen finden Sie in der Java-API-Dokumentation.

Ausgabestreams

Ausgabestreams sind fast immer einem entsprechenden Eingabestream gegenüberzustellen, die Sie bereits kennengelernt haben. Wenn eine InputStream-Klasse eine bestimmte Operation ausführt, führt die entsprechende OutputStream-Klasse die umgekehrte Operation aus. In diesem Abschnitt werden die folgenden Ausgabestream-Klassen vorgestellt:


Vergessen Sie nicht, oben in jeder Quellcodedatei, die Sie erzeugen, die folgende Zeile einzufügen, sonst funktionieren die Beispiele aus diesem Abschnitt nicht.

import java.io.*

Die abstrakte Klasse OutputStream

OutputStream ist eine abstrakte Klasse, die definiert, wie ein Erzeuger einen Bytestrom auf ein Ziel schreibt. Die Identität dieses Ziels und die Art und Weise, wie die Bytes transportiert werden, ist irrelevant. Wenn Sie einen Ausgabestream verwenden, ist er die Quelle dieser Bytes, und das ist alles, was Ihr Programm wissen muß.

Alle Ausgabestreams leiten sich von der abstrakten Klasse OutputStream ab. Alle verwenden die in diesem Abschnitt beschriebenen Methoden. Die Methode write() stellt die grundlegende Funktionalität in der abstrakten Klasse bereit. Die Methoden flush() und close() sind Gerüste, die in Unterklassen überschrieben werden müssen, wenn sie eine sinnvolle Aufgabe erfüllen sollen.

write()

Die wichtigste Methode, die der Erzeuger eines Ausgabestreams hat, ist diejenige, die Bytes auf das Ziel schreibt. Die write()-Methode liegt in verschiedenen Versionen vor, die jweils blockieren, bis das erste Byte geschrieben ist.


Machen Sie sich keine Sorgen über diese scheinbare Einschränkung. Warum, das können Sie im Abschnitt über die read()-Methode von InputStream nachlesen.

Die erste Form der Methode write() schreibt ein Byte Daten:

OutputStream s = getAnOutputStream ();
while (thereAreMoreBytesToOutput()) {
byte b = getNextByteForOutput();
s.write(b);
}

Hier ein Beispiel für die zweite Form von write(), die einen Puffernamen als einziges Argument entgegennimmt:

byte[] outBuff = new byte[1024]; // eine beliebige Größe ist ok
fillInData(outBuff): // die auszugebenden Daten
s.write(outBuff);

Diese Form von write() versucht, den gesamten Puffer auszugeben, der ihr übergeben wurde. Weil es sich bei diesem Puffer um ein Byte-Array handelt, können Sie einen Offset dafür angeben, ebenso wie die Anzahl der zu schreibenden Bytes:

s.write(outBuff, 101, 300);

Dieses Beispiel schreibt die Bytes 101 bis 400 und verhält sich sonst genau wie die zweite write()-Methode, die gerade vorgestellt wurde. Die Standardimplementierung dieser write()-Methode macht gnau das - sie verwendet 0 als Offset und b.length (Pufferlänge) als die Anzahl der zu schreibenden Bytes.

flush()

Weil Sie nicht unbedingt wissen müssen, womit ein Ausgabestream verbunden ist, müssen Sie ihn vielleicht durch einen gepufferten Cache schicken, damit er korrekt geschrieben wird. Die flush()-Version von OutputStream tut nichts, aber sie wird von Unterklassen überschrieben, die diese Funktionalität erwarten (z.B. BufferedOutputStream und PrintStream).

close()

Wie einen InputStream sollten Sie auch einen OutputStream explizit schließen, so daß er alle Ressourcen freigibt, die er möglicherweise belegt hat. Die Hinweise aus dem Abschnitt über die close()-Methode von InputStream gelten auch hier. In OutputStream macht die close()-Methode nichts. Sie muß in einer Unterklasse überschrieben werden, damit sie eine sinnvolle Funktionalität aufweist.

ByteArrayOutputStream

Diese Methode ist die Umkehrung von ByteArrayInputStream. Sie schreibt Ausgaben in ein Byte-Array:

OutputStream s = new ByteArrayOutputStream();
s.write(123);
...

Die Größe des internen Byte-Arrays wächst, um einen Stream beliebiger Größe abzulegen. Sie können aber auch eine Kapazität vorgeben, wenn Sie das möchten:

OutputStream s = new ByteArrayOutputStream(1024 * 1024); // 1 MB


Jetzt haben Sie ein Beispiel für einen Ausgabestream gesehen. Diese neuen Streams werden mit der einfachsten aller möglichen Datenziele verbunden, einem Array mit Bytes im Speicher des lokalen Computers.

Nachdem der ByteArrayOutputStream s gefüllt ist, kann er mit der Methode writeTo() an einen anderen Ausgabestream gesendet werden:

OutputStream secondOutputStream = getFirstOutputStream();
ByteArrayOutputStream s = new ByteArrayOutputStream();
fillWithUsefulData(s);
s.writeTo(secondOutputStream);

Er kann auch als Byte-Array extrahiert oder in einen String konvertiert werden:

byte[] buffer = s.toByteArray();
String bufferString = s.toString();
String bufferEncodedString = s.toString(charEncoding);

Diese letzte Methode ermöglicht Ihnen, Daten unter Verwendung der im Argument charEncoding spezifizierten Codierung in einen String umzuwandeln.

ByteArrayOutputStream besitzt auch zwei Utility-Methodeen. Die Methode size() gibt die Anzahl der Bytes zurück, die im internen Byte-Array abgelegt sind, und reset() ermöglicht, daß der Stream ohne Neuallozierung des Speichers wiederverwendet wird.

int sizeOfMyByteArray = s.size(); // gibt die aktuelle Größe zurück
s.reset(); // s.size() würde jetzt 0 zurückgeben
s.write(123);
...

FileOutputStream

Einer der gebräuchlichsten Verwendungszwecke von Streams ist, sie Dateien im Dateisystem zuzuordnen.


Ein Versuch, von einem Applet aus auf Streams zuzugreifen, die auf Dateien basieren, kann Sicherheitsprobleme verursachen, abhängig von der Sicherheitsebene, die der Anwender auf seinem Browser eingestellt hat. Weitere Informationen entnehmen Sie den Erläuterungen im Abschnit tüber FileInputStream.

Und so wird ein solcher Ausgabestream auf einem UNIX-System erzeugt:

OutputStream s = new FileOuputStream("/some/path/and/filename");

Sie können den Stream auch aus einem zuvor geöffneten Dateideskriptor erzeugen:

int fd = openOutputFile();
OutputStream s = new FileOutputStream(fd);

Weil FileOutputStream die Umkehrung von FileInputStream ist, kennt es auch dieselben Tricks:

FileOutputStream aFOS = new FileOutputStream("aFileName");
int myFD = aFOS.getFD();
/* close() wird von der Speicherbereinigung automatisch aufgerufen */


Um die neuen Methoden aufzurufen, müssen Sie die Stream-Variable aFOS vom Typ FileOutputStream deklarieren, weil OutputStream diese neuen Methoden nicht kennt.

Der erste Teil, getFD(), gibt den Dateideskriptor der Datei zurück, auf der der Stream basiert. Außerdem müssen Sie finalize() und close() nicht direkt aufrufen. Die Speicherbereinigung ruft close() automatisch auf, wenn sie erkennt, daß der Stream nicht mehr gebraucht wird, aber noch vor dem eigentlichen Zerstören des Streams (siehe FileInputStream).

FilterOutputStream

Diese Klasse reicht einfach die Standardmethoden von OutputStream weiter. Sie beinhaltet einen weiteren Stream, definitionsgemäß in der Kette der Filter um eine Stelle weiter unten, an den sie alle Methodenaufrufe weitergibt. Sie implementiert nichts neues, kann aber verschachtelt werden:

OutputStream s = getAnOutputStream();
FilterOutputStream s1 = new FilterOutputStream(s);
FilterOutputStream s2 = new FilterOutputStream(s1);
FilterOutputStream s3 = new FilterOutputStream(s2);
... s3.write(123) ...

Immer wenn für den gefilterten Stream s3 ein write() erfolgt, gibt sie die Anforderung an s2 weiter, s2 macht dasselbe mit s1, und schließlich wird s aufgefordert, die Bytes auszugeben. Unterklassen von FilterOutputstream sollten natürlich etwas sinnvolles mit den Bytes machen, wenn sie die Klasse durchlaufen (siehe FilterInputStream).

Diese Klasse macht zwar selbst nicht viel, aber sie ist public deklariert nicht abstract. Das heißt, sie ist zwar selbst nutzlos, aber Sie können sie direkt verwenden. Um etwas sinnvolles zu machen, sollten Sie jedoch eine der Unterklassen verwenden, die in den folgenden Abschnitten beschrieben sind.

BufferedOutputStream

Dies ist einer der praktischsten alller Streams. Er implementiert alle Methoden von OutputStream, stellt aber dazu ein gepuffertes Byte-Array zur Verfügung, das als Cache für zukünftige Schreiboperationen genutzt werden kann. Damit sind Sie nicht mehr von der Größe der Abschnitte abhängig, die Sie schreiben (beispielsweise auf Peripheriegeräte, Dateien oder Netzwerke). Außerdem ermöglicht er intelligente Streams, die weiterlesen, wenn sie erwarten, daß Sie bald mehr Daten brauchen.

Weil die Pufferung von BufferedOutputStream so praktisch ist und weil nur hier flush() voll implementiert wird, wollen Sie vielleicht, daß jeder Ausgabestream diese Funktionalität nutzen kann. Glücklicherweise können Sie jeden Ausgabestream mit BufferedOutputStream umschließen, um genau das zu realisieren:

OutputStream s = new BufferedOutputStream(new FileOutputStream("myfile"));

Damit erhalten Sie einen gepufferten Ausgabestream, basierend auf myFile, der flush() richtig einsetzt. Wie bei gefilterten Eingabestreams kann die gesamte Funktionalität durch Verschachtelung an beliebige Ausgabestreams weitergegeben werden.

DataOutputStream and DataOutput

Alle Methoden der Klasse DataOutputStream überschreiben die in der Schnittstelle DataOutput definierten abstrakten Methoden. Diese Schnittstelle ist so allgemein, daß Sie sie in Ihren eigenen Klassen wiederverwenden können.

In Kombination mit der Schnittstelle DataInput stellt DataOutput Methoden bereit, die zum Lesen und Schreiben benutzt werden können und einen komplexeren, typisierten Datenstream realisieren. Hier die Methodensignaturen, die diese Schnittstelle definieren:

void write(int i) throws IOException;
void write(byte buf[]) throws IOException;
void write(byte buf[], int off, int len) throws IOException;

void writeBoolean(boolean b) throws IOException;
void writeByte(int i) throws IOException;
void writeShort(int i) throws IOException;
void writeChar(int i) throws IOException;
void writeInt(int i) throws IOException;
void writeLong(long l) throws IOException;
void writeFloat(float f) throws IOException;
void writeDouble(double d) throws IOException;
void writeBytes(String s) throws IOException;
void writeChars(String s) throws IOException;
void writeUTF(String s) throws IOException;

Die meisten dieser Methoden haben Gegenstücke in der DataInput-Schnittstelle. Die ersten drei Methoden reflektieren die write()-Methoden, die Sie schon kennengelernt haben. Jede der nächsten acht Methoden schreibt einen elementaren Datentyp. Die letzten drei Methoden schreiben einen String mit Bytes oder Zeichen auf den Steram. Die Methode writeBytes() schreibt 8-Bit-Bytes. Die Methode writeChars() schreibt 16-Bit-Unicode-Zeichen. Die Methode writeUTF() schreibt einen speziellen Unicode-Stream (der mit readUTF() gelesen wird).

DataOutputStream implementiert die DataOutput-Schnittstelle - d.h. DataOutputStream stellt konkrete Definitionen für die abstrakten Methoden von DataOutput zur Verfügung. Jetzt wollen wir das Ganze in der Praxis betrachten:

DataOutputStream s = new DataOutputStream(getNumericOutputStream());
long size = getNumberOfItemsInNumbericStream);
s.writeLong(); // Anzahl der Elemente im Stream
for (int i = 0; i < size; ++i) {
if (shouldProcessNumber(i)) { // soll ich das Element verarbeiten?
s.writeBoolean(true);
s.writeInt(theIntegerForItemNumber(i));
s.writeShort(theMagicBitFlagsForItemNumber(i));
s.writeDouble(theDoubleForItemNumber(i));
}
else
s.writeBoolean(false);
}

Das ist die Umkehrung zu dem Beispiel für DataInput. Zusammen bilden sie ein Paar, das ein Array strukturierter elementarer Typen über jeden Stream (oder jede Transportschicht) schicken kann. An diesem Beispiel können Sie sich für eigene Implementierungen orientieren.

Neben der eben vorgestellten Schnittstelle implementiert die Klasse auch eine Utility-Methode, die die Anzahl der Bytes zurückgibt, die bis zu diesem Zeitpunkt geschrieben wurden:

int theNumberOfBytesWrittenSoFar = s.size

PrintStream

Im JDK 1.1 ist die Klasse PrintStream veraltet und wurde größtenteils durch PrintWriter ersetzt.

Es gibt jedoch noch einige Methoden im JDK 1.1, die diese Klasse verwenden. Sie erkennen es vielleicht noch nicht, aber Sie sind mit zwei dieser Methoden bereits vertraut. Immer wenn Sie die folgenden Methoden aufrufen:

System.out.print(...);
System.out.println(...);

verwenden Sie eigentlich eine PrintStream-Instanz, die sich in der System-Klassenvariablen out befindet, um die Ausgabe vorzunehmen. (System.err ist ebenfalls ein PrintStream). PrintStream hat kein Gegenstück in InputStream. Eine weitere Unterklasse, die auf dieser Klasse basiert, ist java.rmi.server.LogStream, die das Protokoll von einem entfernt gelegenen Server ausgibt.

java.security.DigestOutputStream

Diese Klasse ist zwar im Paket java.security implementiert, aber sie leitet sich von FilterOutputStream ab. Diese Stream-Klasse erzeugt die Ausgabe für ein java.security.MessageDigest-Objekt. Sie kann aktiviert oder deaktiviert werden. Wenn der Stream aktiviert ist, aktualisiert ein write() das Digest, wenn er deaktiviert ist, wird das Digest nicht aktualisiert. Sie hat die folgende Form:

DigestOutputStream(anOutputStream, aMessageDigest)

dabei ist anOutputStream ein OutputStream oder eine Ableitung davon, und aMessageDigest ist das Digest, das diesem Stream zugeordnet ist. Weitere Informationen finden Sie in der Java-API-Dokumentation.

java.util.zip.CheckedOutputStream

Diese Klasse ist zwar in java.util.zip implementiert, leitet sich aber von FilterOutputStream ab. Sie hat die Aufgabe, einen Stream zu erzeugen, der eine Prüfsumme der gelesenen Daten verwalten kann. Ihr Konstruktor hat die folgende Form:

CheckedOutputStream(anOutputStream, aChecksum)

dabei ist anOutputStream eine OutputStream-Klasse oder eine Unterklasse davon, und aChecksum ist CRC32 oder Adler32. Weitere Informationen finden Sie in der Java-API-Dokumentation.

java.util.zip.DeflaterOutputStream

Diese Klasse ist zwar in java.util.zip implementiert, leitet sich aber von FilterOutputStream ab. Sie hat die Aufgabe, einen Stream zu erzeuen, um Daten zu komprimieren. Sie besitzt zwei Konstruktoren, die die folgende Form aufweisen:

DeflaterOutputStream(anOutputStream, aDeflater)
DeflaterOutputStream(anOutputStream, aDeflater, theSize)

dabei ist anOutputStream eine OutputStream-Klasse oder- Unterklasse, und aDeflator ist die verwendete Komprimierung. Der erste Konstruktor erzeugt einen Ausgabestream mit einer Standardpuffergröße, der zweite ermöglicht Ihnen die Größe im Argument theSize anzugeben. Diese Klasse hat zwei Unterklassen: java.util.zip.GZIPOutputStream, die komprimierte Daten im GZIP-Format schreibt, und java.util.zip.ZipOutputStream, die komprimierte Daten im ZIP-Dateiformat schreibt (und sie implementiert java.util.zip.ZipConstants). Hier die Konstruktoren:

GZIPOutputStream(anOutputStream)
GZIPOutputStream(anOutputStream, theSize)
ZIPOutputStream(anOutputStream)

Wie Sie sehen, haben diese Konstruktoren kein aDeflater-Argument, weil sie spezifisch für bestimmte Komprimierungsformate sind (die beiden ersten für GZIP, das letzte für ZIP). Darüber hinaus hat ZIPOutputStream keinen Konstruktor, in dem Sie die Puffergröße angeben könnten.

Weitere Informationen finden Sie in der Java-API-Dokumentation.

ObjectOutputStream

ObjectOutputStream implementiert die Schnittstellen java.io.ObjectOutput und java.io.ObjectStreamConstants und wird für die Serialisierung von elementaren Daten und Graphen von Objekten verwendet, die später deserialisiert (gespeichert) werden können. Zusammen mit ObjektInputStream bildet ObjectOutputStream eine Möglichkeit, Ihre Objekte persistent zu speichern, beispielsweise:

FileOutputStream FOS = new FileOutputStream("myfile");
ObjectOutputStream OOS = new ObjectOutputStream(FOS);
OOS.writeObject("Today is: ");
OOS.writeObject(new Date());
OOS.flush()
FOS.close();

Hier schreibt ObjectOutputStream den Satz »Today is: » und das Systemdatum in die Datei myFile. Wenn die Daten von einem ObjectInputStream gelesen werden, bleibt das Datum in seinem Originalformat und wird als Date-Objekt erkannt:

FileInputStream FIS = new FileInputStream("myfile");
ObjectInputStream OIS = new ObjectInputStream(FIS);
String today = (String).OIS.readObject();
Date date = (Date)OIS.readObject();
FIS.close();

PipedOutputStream

PipedOutputStream unterstützt (zusammen mit PipedInputStream) eine UNIX-ähnliche Pipe-Verbindung zwischen zwei Threads und realisiert dabei die Synchronisierung, diees Konstrukt bietet. Um die Verbindung einzurichten, verwenden Sie den folgenden Code:

PipedInputStream sIn = PipedInputStream();
PipedOutputStream sOut = PipedOutputStream(sIn);

Ein Thread schreibt in sOut, der andere liest von sIn. Durch Einrichtung solcher Paare können die Threads in beiden Richtungen sicher kommunizieren.

Reader

Die Klasse Reader realisiert dieselben Ziele wie InputStream, arbeitet aber nicht mit Bytes, sondern mit Zeichen. Es gibt drei read()-Methoden, die parallel zu denen von InputStream() sind:

read();
read(cBuff[]);
read(cBuff[], offset, length);

Die erste liest ein einzelnes Zeichen; die zweite liest ein Zeichen-Array, cBuff; die dritte liest einen Teil des Zeichen-Arrays cBuff, beginnend am Offset offset und über length Zeichen. Die Methode skip() nimmt einen long als Argument entgegen.

Darüber hinaus gibt es eine neue Methode, ready(). Diese Methode ergibt true, wenn das nächste read() guarantiert nicht blockiert, andernfalls false.

BufferedReader

Diese Unterklasse ist BufferedInputStream vergleichbar und beinhaltet die Methode ready(), die von der Oberklasse Reader geerbt wird.

LineNumberReader

In einem Editor oder Debugger ist die Zeilennumerierung wichtig. Um Ihren Programmen diese Funktion hinzuzufügen, verwenden Sie die Klasse LineNumberReader, die die Zeilennummern verwaltet. Diese Klasse ist so intelligent, daß sie sich eine Zeilennummer merkt und sie später bei mark() und reset() wiederherstellen kann. Sie könnten diese Klasse wie folgt verwenden:

LineNumberInputStream aLNIS;
aLNIS = new LineNumberInputStream(new FileInputStream("source"));
DataInputStream s = newDataInputStream(aLNIS);
String line;
while ((line = s.readLine()) != null) {
... // die Zeile verarbeiten
System.out.println("Jetzt Zeile " + aLNIS.getLineNumber());
}

Hier werden die beiden Streams um FileInputStream verschachtelt, die die Daten bereitstellen - einer zum Lesen der Zeilen und ein weiterer zur Verwaltung der Zeilennummern. Sie müssen den Zwischenstream explizit benennen, aLNIS, andernfalls könnten Sie später getLineNumber() nicht aufrufen. Wenn Sie die Reihenfolge der verschachtelten Streams umkehren, können beim Lesen von DataInputStream die Zeilen nicht verwaltet werden. Der Stream, der die Daten holt, muß außerhalb der Klasse LineNumberReader liegen, damit er die ankommenden Daten korrekt beobachten kann.

LineNumberReader kann auch zum setzen von Zeilennummern verwendet werden, setLineNumber(), wenn Sie das brauchen.

CharArrayReader

Die Klasse CharArrayReader liest aus einem Zeichen-Array, ähnlich wie ByteArrayInputStream. Sie liest entweder das ganze Array oder einen bestimmten Abschnitt:

CharArrayReader(cBuff[]);
CharArrayReader(cBuff[], offset, length);

cBuff ist das Zeichen-Array, aus dem gelesen wird, offset ist der Index des ersten Zeichens, das gelesen werden soll, length ist die Anzahl der zu lesendenZeichen. CharArrayReader implementiert alle Reader-Methoden, auch mark(), reset() und ready().

FilterReader

FilterReader ist eine abstrakte Klasse zum Lesen gefilterter Zeichenstreams, die mark() und reset() unterstützt. Aktuell wird nur die Unterklasse PushbackReader implementiert.

PushbackReader

Der Stream PushbackReader wird zum Zurücklesen von Daten in den Stream verwendet, aus dem sie kamen, was vor allem in Parsern praktisch ist. Sie können einen 1-Zeichen-Pushback-Puffer verwenden oder eine beliebige Größe dafür angeben. Neben den read()-Methodeen stellt diese Unterklasse drei unread()-Methode bereit und verwendet eine vereinfachte Verison zum Markieren und Zurücksetzen, um die Position zu verwalten.

InputStreamReader

Diese Klasse stellt die Verbindung zwischen Byte-Streams und Zeichen-Streams dar. InputStreamReader liest ein Byte und wandelt es basierend auf der vorgegebenen Codierung in ein Zeichen um. Wenn keine Codierung vorgegeben ist, wird die Standard-Codierung der Plattform verwendet. Am besten wird sie in einen gepufferten Reader eingebettet:

BufferedReader myBuffRead = new BufferedReader
(new InputStreamReader(System.in));

InputStreamReader implementiert zusätzlich zu den Standard-Reader-Methoden die Methode getEncoding(), die den Namen der Codierung zurückgibt, die der Stream aktuell verwendet.

FileReader

Die Klasse FileReader liest Zeichendateien unter Verwendung der Standard-Zeichencodierung und der Standardpuffergröße. Sie hat drei Konstruktoren:

FileReader(aFileName);
FileReader(aFile);
FileReader(aFD);

aFileName stellt einen Dateinamen vom Typ String dar; aFile ist ein Dateiname vom Typ File, und aFD ist ein Dateideskritor.

PipedReader

PipedReader und seine komplementäre Klasse PipedWriter bilden gemeinsam eine Zweiwege-Kommunikation zwischen Threads. Diese beiden Klassen sind PipedInputStream und PipedOutputStream sehr ähnlich, die später noch beschrieben werden.

StringReader

Die Klasse StringReader liest aus einem String-Objekt. Sie kann entweder den gesamten String oder einen Abschnitt davon lesen:

StringReader(aStr);
StringReader(aStr, offset, length);

aStr ist das String-Objekt, aus dem gelesen wird, offset ist der Index des ersten zu lesenden Zeichens, und length ist die Anzahl der Zeichen, die gelesen werden. StringReader implementiert außerdem mark(), reset(), ready() und skip().

Writer

Diese Klasse ist die komplementäre Klasse zu Reader und verfolgt dieselben Ziele wie OutputStream. Statt Bytes verwendet die Klasse jedoch Zeichen. Es gibt fünf write()-Methoden, drei davon sind parallel zu denen von OutputStream realisiert, und zwei davon sind für String-Objekte ausgelegt.

Die folgende write()-Methode schreibt das gesamte Zeichen-Array in cBuff:

write(cBuff[]);

Diese write()-Methode ist als abstract deklariert und wird in der Writer-Unterklasse überschrieben (wie etwa BufferedWriter im nächsten Abschnitt), um sinnvoll zu arbeiten:

write(cBuff[], offset, length);

In dieser Version der write()-Methode ist anInt ein Integer-Argument, dessen untere 16 Bits als einzelnes Zeichen geschrieben wird (die oberen 16 Bit werden ignoriert):

write(anInt);

In dieser write()-Methode gibt das aStr-Argument das zu schreibende String-Objekt an:

write(aStr);

Weil Strings auch als Arrays angesprochen werden können, nimmt die folgende Version von write() ein offset-Argument entgegen, das das erste Zeichen angibt, das im String-Objekt aStr geschrieben werden soll, sowie ein length-Argument, das angibt, wie viele Zeichen geschrieben werden sollen:

write(aStr, offset, length);

Die Methoden close() und flush() werden als abstract deklariert und müssen von den Unterklassen überschrieben werden.

BufferedWriter

Die Unterklasse BufferedWriter ist BufferedOutputStream vergleichbar. Sie überschreibt drei von der Oberklasse Writer geerbten write()-Methoden, um ihre Funktionalität mit der von dieser Klasse bereitgestellten Pufferung zu kombinieren.

write(cBuff[], offset, length);
write(anInt);
write(aStr, offset, length);

Die erste write()-Methode stellt außerdem die grundlegende Funktionalität bereit, einen Teil des Zeichen-Arrays zu schreiben, weil sie in der Oberklasse Writer als abstract deklariert ist.

Außerdem überschreibt diese Klasse die Methoden flush() und close() für die Pufferung. Der Ausgabepuffer wächst automatisch entsprechend der geschriebenen Zeichen.

CharArrayWriter

Diese Klasse schreibt ein Zeichen-Array, vergleichbar ByteArrayOutputStream. Sie kann ein ganzes Array oder einen bestimmten Teil davon schreiben:

CharArrayWriter(cBuff[]);
CharArrayWriter(cBuff[], offset, length);
CharArrayWriter(aStr, offset, length);

cBuff ist das Zeichen-Array, in das geschrieben werden soll, aStr ist das String-Objekt, in das geschrieben werden soll; offset ist der Index des ersten zu schreibenden Zeichens; length ist die Anzahl der zu schreibenden Zeichen. Auch hier wächst der Ausgabepuffer automatisch nach Bedarf.

CharArrayWriter implementiert eine weitere Methode:

writeTo(aWriter);

aWriter ist ist ein Zeichenstream der Writer-Klasse, in den die Zeichen geschrieben werden sollen.

FilterWriter

FilterWriter ist eine abstrakte Klasse zum Schreiben gefilterter Zeichen-Streams. Aktuell sind keine Unterklassen implementiert. Die Klasse deklariert jedoch die Variable out, die Ihnen ermöglicht, unter Verwendung dieser Variablen eigene gefilterte Stream-Unterklassen zu implementieren, um auf den zugrunde liegenden Ausgabestream zuzugreifen. Sie überschreibt die Methoden flush() und close() sowie die folgenden drei write()-Methoden:

write(cBuff[], offset, length);
write(anInt);
write(aStr, offset, length);

OutputStreamWriter

Die Klasse OutputStreamWriter stellt die Ausgabeverbindung zwischen Zeichen-Streams und Byte-Streams bereit. Diese Klasse liest die einzelnen Zeichen ein und übersetzt sie gemäß der vorgegebenen Codierung in Bytes. Wenn keine Codierung angegeben ist, wird die Standard-Codierung der Plattform verwendet. Am besten wird sie in einen gepufferten Writer eingehüllt:

BufferedWriter myBuffWrite = new BufferedWriter
(new OutputStreamWriter(System.out));

OutputStreamWriter implementiert eine weitere Methode, getEncoding(), die den Namen der Codierung zurückgibt, die der Stream aktuell verwendet.

FileWriter

FileWriter schreibt unter Verwendung der Standard-Zeichencodierung und der Standard-Puffergröße in Dateien. Sie hat vier Konstruktoren:

FileWriter(aFile);
FileWriter(aFD);
FileWriter(aStr);
FileWriter(aStr, aBool);

aFile ist ein Dateiname vom Typ File, aFD ist ein Dateideskriptor, aStr ist ein String-Objekt, das einen Dateinamen darstellt. Das Argument aBool des vierten Konstruktors ist ein Boolescher Wert; true gibt an, daß der Stream angefügt werden soll, false bewirkt, daß er überschrieben wird.

PipedWriter

PipedWriter unterstützt (zusammen mit PipedReader) eine UNIX-ähnliche Pipe-Verbindung zwischen zwei Threads, die die Synchronisierung dieses Konstrukts realisieren. Um die Verbindung einzurichten, vrwenden Sie folgenden Code:

PipedReader sRead = PipedReader();
PipedWriter sWrite = PipedWriter(sRead);

Ein Thread schreibt in sWrite, der andere liest aus sRead. Durch Einrichtung solcher Paare können die Threads sicher in beide Richtungen kommunizieren.

PrintWriter

Die Klasse PrintWriter ersetzt die veraltete Klasse PrintStream und implementiert all ihre Methoden. Weil sie in der Regel einer Bildschirmausgabe zugeordnet ist, stellt sie eine Implementierung der flush()-Methode bereit. Außerdem bietet sie die Methoden write() und close() sowie viele Auswahlmöglichkeiten für die Ausgabe von elementaren Datentypen und String-Objekten:

public void write(char buf[]);
public void write(char buf[], int off, int len);
public void write(int c);
public void write(String s);
public void write(String s, int off, int len);

public void close();
public void flush();

public void print(boolean b);
public void print(char c);
public void print(char s[]);
public void print(double d);
public void print(float f);
public void print(int i);
public void print(long l);
public void print(Object obj);
public void print(String s);

public void println(); // gibt nur ein Neuezeile-Zeichen aus
public void println(boolean x);
public void println(char x);
public void println(char x[]);
public void println(double x);
public void println(float x);
public void println(int x);
public void println(long x);
public void println(Object x);
public void println(String x);

Die erste println()-Methode gibt ein Neuezeile-Zeichen aus; alle anderen rufen die entsprechenden print()-Methoden auf und geben dann ein Neuezeile-Zeichen aus. PrintWriter kann um jeden Ausgabestream gebunden werden, ähnlich einer Filterklasse:

PrintWriter s = new PrintWriter(new FileOutputStream("myfile"));
s.println("Here's the first line of the text to write to myfile.");

Es gibt vier Konstruktoren für diese Klasse:

public PrintWriter(Writer out);
public PrintWriter(Writer out, boolean autoFlush);
public PrintWriter(OutputStream out);
public PrintWriter(OutputStream out, boolean autoFlush);

Diese Klasse wirft keine Ausnahmen auf, um den Fehlerstatus zu ermitteln, müssen Sie die Methode checkError() aufrufen. Die Methode leert den Stream und gibt true zurück, wenn ein Fehler in diesem oder einem vorhergehenden Aufruf aufgetreten ist.

StringWriter

Diese Klasse schreibt ein String-Objekt, ein Zeichen oder ein Zeichen-Array. Sie kann einen ganzen String oder einen Teil eines Strings schreiben:

StringWriter(aStr);
StringWriter(aStr, offset, length);

aStr ist das String-Objekt, das geschrieben werden soll; offset ist der Index des ersten zu schreibenden Zeichens; length ist die Anzahl der zu schreibenden zeichen. Es kann auch ein einzelnes Zeichen geschrieben werden:

StringWriter(anInt);

anInt ist ein Integer-Argument, dessen untere 16 Bits als einzelnes Zeichen geschrieben werden (die oberen 16 Bits werden ignoriert). Darüber hinaus kann auch ein Teil eines Zeichen-Arrays geschrieben werden:

StringWriter(cBuff[], offset, length);

cBuff ist das Zeichen-Array, in das geschrieben weden soll, offset ist der Index des ersten zu schreibenden Zeichens, und length ist die Anzahl der zu schreibenden Zeichen.

Dateiklassen

Das Paket java.io implementiert drei Klassen, die Java eine abstrakte Definition zur Verarbeitung verschiedener Aspekte plattformunabhängiger Dateinamenkonventionen bereitstellen, unter anderem:

File

Die Klasse File hat drei Konstruktoren:

File(aFile, aStr);
File(bStr);
File(cStr, dStr);

Der erste erzeugt das File-Objekt aStr aus dem Verzeichnis-File-Objekt, das in aFile angegeben ist. Der zweite erzeugt das File-Objekt bStr. Der dritte erzeugt das File-Objekt cStr.

Diese Klasse definiert außerdem vier Variablen: separator, einen Dateinamen-Separator-String, separatorChar, ein Dateinamen-Separator-Zeichen, pathSeparator, einen Pfadnamen-Separator-String und pathSeparatorChar, ein Pfadnamen-Separator-Zeichen. Diese Variablen ermöglichen Ihnen, systemspezifische Separatoren zu berücksichtigen.

Darüber hinaus definiert die Klasse viele Methoden, die Ihnen ermöglichen, Datei- und Pfadnamen zu manipulieren. canRead() und canWrite() sind Boolesche Werte, die angeben, ob eine Datei gelesen werden kann bzw. ob in sie geschrieben werden kann die Methode equals() führt einen Objekt-Vergleich durch. exists() ist eine Boolesche Methode, die angibt, ob eine Datei existiert, und isDirectory() gibt an, ob ein Verzeichnis existiert.

Es gibt mehrere Methoden, die Teile von Datei- und Pfadnamen zurückgeben, deren Namen selbsterklärend sind: getAbsolutePath(), getCanonicalPath(), getName(), getParent() und getPath(). Andere Methoden geben die Dateiattribute zurück: isAbsolute() gibt an, ob der Dateiname absolut ist, isFile() gibt true zurück, wenn eine normale Datei existiert; lastModified() gibt den Zeitstempel der letzten Änderung zurück, und length die Länge der Datei.

Außerdem stehen einige Utility-Methoden zur Verfügung. Die Methoden mkDir() und mkDirs() erzeugen ein Verzeichnis oder mehrere Verzeichnisebenen. Die Methode renameTo() versucht, eine Datei umzubenennen und gibt einen Booleschen Wert zurück, der angibt, ob dieser Versuch erfolgreich war. Die Methode list() listet die Dateien in einem Verzeichnis auf. Die Methode delete() löscht eine Datei vom System. Die Methode hashCode() berechnet den Hashcode für die Datei. toString() schließlich gibt ein String-Objekt zurück, das den Pfadnamen der Datei enthält.

Um diese Methoden neu zu definieren oder um zusätzliche Methoden bereitzustellen, bilden Sie einfach eine Unterklasse von File und überschreiben ihre Methoden oder definieren eigene.

FileDescriptor

Die Klasse FileDescriptor stellt einen Mechanismus für den Zugriff auf eine systemspezifische Struktur bereit, die Zugriff auf eine offene Datei oder ein Socket bietet. Die Java-Dokumentation betont, daß Ihre Anwendung keine eigenen Dateideskriptoren erzeugen sollte, das übernimmt die VM automatisch für Sie. Diese Klasse hat einen Konstruktor:

FileDescriptor();

Dieser Konstruktor wird vom Java-Interpreter verwendet, um die Klassenvariablen systemunabhängig zu initialisieren. Die vier Variablen, die er initialisiert, sind fd, der Handle für den Dateideskriptor, in, der Handle für den Standard-Eingabestream, out, der Handle für den Standard-Ausgabestream, und err, der Handle für den Standard-Fehlerstream.

Es gibt auch zwei Methoden: valid(), das true ergibt, wenn der Dateideskriptor auf eine geöffnete Datei oder ein Socket zeigt, und sync(), das eine Synchronisierung aller System-Puffer mit dem physischen Speichermedium zwingt, so daß Sie das Dateisystem in einem bekannten Status überführen können.

RandomAccessFile

Die wichtigste Aufgabe der Klasse RandomAccessFile ist, eine Möglichkeit zu schaffen, Zugriff auf eine Datei zu bieten, mit der Möglichkeit, sich in der Datei unter Verwendung eines Dateizeigers zu bewegen und diese zu manipulieren. Sie implementiert die Schnittstellen DataInput und DataOutput, die heute bereits beschrieben wurden, sowie ihre unzähligen read()- und write()-Methoden.

Diese Klasse hat zwei Konstruktoren:

RandomAccessFile(aStr, alaMode);
RandomAccessFile(aFile, alaMode);

dabei ist aStr ein String-Objekt, das einen Dateinamen repräsentiert. aFile ist ein File-Objekt, und alaMode ist entweder rw, beschreibt also eine read-write-Datei, oder r, für eine read-only-Datei.

Neben den read()- und write()-Methoden dieser Implementierung der DataInput- und DataOutput-Schnittstellen implementiert RandomAccessFile mehrere Utility-Methoden: getFD() gibt den Dateideskriptor zurück, skipBytes() spezifiziert die Anzahl der zu überspringenden Bytes in der Datei, getFilePointer() gibt die aktuelle Position des Dateizeigers zurück, seek() setzt die Position des Dateizeigers auf einen bestimmten Wert, length() gibt die Länge der Datei zurück, und close() schließt die Datei. Interessanterweise gibt es keine open()-Methode. Für diesen Zweck wird die Methode getFD() verwendet, weil sie den Dateideskriptor einer bereits geöffneten Datei zurückgibt.

Verwandte Klassen

Es gibt einige Schnittstellen und Klassen im Paket java.io, die bisher nicht vorgestellt wurden, etwa Serializable, die bereits mehrere Male erwähnt wurde. In diesem Abschnitt werden die Schnittstellen Serializable, Externalizable, FilenameFilter und ObjectInputValidation vorgestellt, ebenso wie die Klassen ObjectStreamClass und StreamTokenizer.

Schnittstellen

Serializable speichert, wie bereits erwähnt, den Status von Objekten und Daten beim Streaming. Wenn eine Klasse diese Schnittstelle implementiert, werden die Objekte und Daten dieser Klasse serialisiert, wenn sie gespeichert wird, und deserialisiert, wenn sie aus einem Stream geladen werden. Alle Unterklassen erben diese Funktionalität. Diese Schnittstelle gibt an, daß Klassen, die ihre eigene Serialisierung und Deserialisierung verwenden wollen, zwei private Methoden implementieren sollten, nämlich mit folgenden Signaturen:

void writeObject(ObjectOutputStream out)
throws IOException; {...}

void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException; {...}

Die Schnittstelle ObjectInput erweitert DataInput und definiert die Methode readObjet(), so daß sie Objekte lesen kann. ObjectOutput erweitert DataOutput und definiert die Methode writeObject(), so daß sie Objekte schreiben kann. Zusammen stellen diese Schnittstellen die Funktionalität für die nächste Schnittstelle bereit, Externalizable.

Externalizable erweitert Serializable und definiert zwei eigene Methoden. Die Methode writeExternal() speichert den Inhalt ihres Arguments vom Typ ObjectOutput, indem sie die writeObject()-Methode von ObjectOutput für Objekte oder die Methoden von DataOutput (die von ObjectOutput geerbt werden) für elementare Daten anwendet. Die Methode readExternal() stellt den Inhalt des Arguments vom Typ ObjectOutput wieder her, wozu die readObject()-Methode von ObjectOutput für Objekte oder die DataOutput-Methoden für elementare Datentypen verwendet werden. Das ist die Umkehrung von writeExternal(). Weil Strings und Arrays in Java als Objekte implementiert sind, werden sie von dieser Schnittstelle auch als solche behandelt.

ObjectInputValidation ist eine Callback-Schnittstelle, die die Auswertung von Graphen-Objekten erlaubt, und die ermöglicht, ein Objekt aufzurufen, wenn das Graphen-Objekt die Deserialisierung abgeschlossen hat. Sie definiert eine einzige abstrakte Methode, validateObject(), die eine InvalidObjectException aufwirft, wenn das Objekt sich selbst nicht auswerten kann.

FilenameFilter stellt eine Filter-Schnittstelle für Dateinamen bereit. Sie definiert eine abstrakte Boolesche Methode:

accept(aFile, aStr);

Diese Methode gibt true zurück, wenn der Dateiname aStr in der durch aFile dargestellten Dateiliste enthalten ist.

Klassen

Es gibt zwei java.io-Klassen, die Sie noch nicht gesehen haben: ObjectStreamClass and StreamTokenizer.

ObjectStreamClass implementiert die Schnittstelle Serializable und ermöglicht Ihrer Anwendung, festzustellen, ob eine bestimmte Klasse serialisierbar ist. Dazu verwendet sie die Methode lookup(). Wenn eine Instanz einer Klasse serialisiert wird, können Sie die getSerialVersionUID()-Methode von ObjectStreamClass anwenden, um die serialVersionUID für die Klasse zu ermitteln, die angibt, in welchem Format die Klasse serialisiert wurde.

StreamTokenizer nimmt einen InputStream oder Reader entgegen und wandelt ihn in einen Token-Stream um. Jedes Token kann eines von fünf Attributen haben: Whitespace, numerisch, Zeichen, Strings (in einfachen oder doppelten Anführungszeichen) und Kommentarzeichen.

Jede StreamTokenizer-Instanz hat vier Flags, die angeben, ob die Instanz Zeilenendezeichen als Token oder Whitespace zurückgibt, ob sie Kommentare im C-Stil erkennt (*/), und ob die Bezeichner in Kleinbuchstaben umgewandelt werden. Durch die Bildung von Unterklassen und die Bereitstellung einer zusätzlichen Funktionalität können Sie leistungsfähige lexikalische Parser entwickeln.

Zusammenfassung

Heute haben Sie die Klasse InputStream und ihre Unterklassen kennengelernt, die bytebasierte Eingabestreams realisieren, nämlich für Byte-Arrays, Dateien, Pipes, Stream-Folgen, Objekte und String-Puffer sowie Eingabefilter für die Pufferung, typisierte Daten und das Zurückschreiben von Daten. Sie haben die Klasse OutputStream und ihre Unterklassen kennengelernt, die bytebasierte Ausgabestreams für Byte-Arrays, Pipes und Objekte definieren, ebenso wie Ausgabefilter für das Puffern und typisierte Daten.

Sie haben gesehen, wie die neuen Reader- und Writer-Klassen eine optimierte Verarbeitung zeichenbasierter Streams ermöglichen, mit vielen Methoden und Unterklassen, die zu denen von InputStream und OutputStream analog sind.

Sie haben sich mit den elementaren Methoden vertraut gemacht, die alle Streams verstehen, etwa read() und write() sowie die eindeutigen Methoden, die viele Streams diesem Repertoire hinzufügen. Sie haben gelernt, wie IOExceptions aufgefangen werden, insbesondere EOFException, die anzeigt, wenn das Dateiende erreicht ist.

Außerdem kennen Sie jetzt drei Dateiklassen aus dem Paket java.io, und Sie haben die in dem Paket definierten Schnittstellen kennengelernt, unter anderem DataInput und DataOutput, die die wichtigsten Operationen für die Klassen DataInputStream, DataOutputStream und RandomAccessFile bereitstellen. Andere ermöglichen die Serialisierung und Deserialisierung von Daten, so daß deren Status gespeichert und wiederhergestellt werden kann. Und schließlich haben Sie die Klasse StreamTokenizer kennengelernt, mit der Sie lexikalische Parser realisieren können.

Fragen und Antworten

F Welche Eingabestreams in java.io implementieren die Methoden mark(), reset() und markSupported()?

A Diese Methoden werden zunächst als public in den Klassen InputStream und Reader implementiert; markSupported() ergibt jedoch false, mark() macht gar nichts und reset() wirft einfach eine IOException auf, mit der Meldung, daß mark/reset nicht unterstützt werde.

Die InputStream-Unterklassen BufferedInputStream und ByteArrayInputStream implementieren mark() ind reset(), und markSupported() gibt für Instanzen dieser Unterklassen true zurück. Außerdem gibt markSupported() für eine Instanz von FilterInputStream true zurück, wenn der zugrunde liegende Eingabestream es unterstützt. Die FilterInputStream-Unterklasse PushbackInputStream dagegen überschreibt markSupported() und gibt false zurück.

Von den Reader-Unterklassen implementieren BufferedReader und CharArrayReader mark() und reset(), und markSupported() gibt für Instanzen dieser Unterklassen true zurück. Außerdem gibt markSupported() für eine Instanz von FileReader true zurück, wenn der zugrunde liegende Reader-Stream es unterstützt. Die FilterReader-Unterklasse PushbackReader dagegeb überschreibt markSupported() und gibt false zurück.

F Warum ist die available()-Methode so praktisch, wenn sie manchmal die falsche Antwort zurückgibt?

A erstens gibt sie für viele Streams die korrekte Antwort zurück. Zweitens kann sie für Netzwerkstreams Informationen ermitteln, die Sie auf andere Weise nicht erhalten hätten (etwa die Größe einer Datei, die über ftp übertragen wurde). Wenn Sie eine Fortschrittsanzeige für Dateiübertragungen über das Netzwerk anzeigen, ermittelt available() häufig die Gesamtgröße der Übertragung, und wenn nicht, dann ist das ohnehin offensichtlich für Sie und Ihre Anwender.

F Sie haben die Unterklasse LineNumberInputStream nicht erwähnt, die im JDK 1.0 enthalten war. Steht sie im JDK 1.1 noch zur Verfügung, und wenn nicht, was soll ich statt dessen verwenden?

A Ja, sie ist im JDK 1.1 noch definiert, aber sie wird hier nicht behandelt, weil sie veraltet ist. Java stellt diese Klasse nur noch der Abwärtskompatibilität mit JDK 1.0 halber zur Verfügung, und in neuen Programmen sollte sie überhaupt nicht mehr verwendet werden. Verwenden Sie statt dessen die Klasse LineNumberReader.

Zusammenfassung

Heute haben Sie den Visual Designer kennengelernt und erfahren, wie einfach es ist, mit Drag&Drop eine grafische Benutzeroberfläche anzulegen. Sie haben die Komponenten der AWT-Seite in der Komponentenpalette kennengelernt, ebenso wie ihre wichtigsten Eigenschaften, Methoden und Ereignisse. Die AWT-Komponenten bilden die Grundlage für die anderen Komponenten in der Palette. Morgen lernen Sie die komplexeren JavaBean-Komponenten kennen, die die JBCL (JavaBeans Component Library) von JBuilder bilden.

Quiz

Übung

Erstellen Sie das Programm Cat.java, das die Zeilen der Tastatureingabe unter Verwendung von DataInputStream mit Satnadardeingabe (System.in) und readLine()- und System.out.println()-Methoden auf den Bildschirm ausgibt.


© 1997 SAMS
Ein Imprint des Markt&Technik Buch- und Software- Verlag GmbH
Elektronische Fassung des Titels: JBuilder in 14 Tagen, ISBN: 3-87791-895-6

Previous Page Page Top TOC Index Next Page See Page