MarktundTechnik Home-Page Previous Page TOC Index Next Page See Page



19. Tag:

Daten-Ströme

von Charles L. Perkins

Heute lernen Sie alles über Java-Ströme mit folgenden Schwerpunkten:

Sie lernen auch zwei Stromschnittstellen, die das Lesen und Schreiben getippter Ströme vereinfachen. Ferner lernen Sie mehrere Utility-Klassen kennen, die benutzt werden, um auf das Dateisystem zuzugreifen. Wie beginnen mit einer kurzen Geschichte über Ströme.

Eine der ersten Versionen des Unix-Betriebssystems war die Pipe. Durch Vereinheitlichung aller möglichen Kommunikationsarten in einer einzigen Metapher ebnete Unix den Weg für eine ganze Reihe ähnlicher Neuerungen, die schließlich in der Abstraktion namens Streams oder Ströme gipfelten.

Eine Pipe ist ein nichtinterpretierter Strom von Bytes, der zur Kommunikation zwischen Programmen (bzw. »gegabelten« Kopien eines Programms) oder zum Lesen und Schreiben von verschiedenen Geräten und Dateien benutzt wird.

Ein Strom ist ein Kommunikationspfad zwischen der Quelle und dem Ziel eines Informationsblocks.

Dieser Informationsblock, d. h. ein nicht interpretierter Bytestrom, kann von jeder »Pipe-Quelle«, dem Rechnerspeicher oder auch vom Internet kommen. Quelle und Ziel eines Stroms sind völlig arbiträre Erzeuger bzw. Verbraucher von Bytes. Darin liegt die Leistung der Abstraktion. Sie müssen beim Lesen nichts über die Quelle und beim Schreiben nichts über das Ziel des Informationsstroms wissen.

Allgemeine Methoden, die von jeder beliebigen Quelle lesen können, akzeptieren ein Stromargument, das die Quelle bezeichnet. Allgemeine Methoden zum Schreiben akzeptieren einen Strom, um das Ziel zu bestimmen. Arbiträre Prozessoren (oder Filter) haben zwei Stromargumente. Sie lesen vom ersten, verarbeiten die Daten und schreiben die Ergebnisse in den zweiten. Diese Prozessoren kennen weder Quelle noch Ziel der Daten, die sie verarbeiten. Quelle und Ziel können sehr unterschiedlich sein: von zwei Speicherpuffern auf dem gleichen lokalen Rechner über ELF-Übertragungen von und zu einer Unterwasserstation bis zu Echtzeit-Datenströmen einer NASA-Sonde im Weltraum.

Durch Abkoppeln des Verbrauchs, der Verarbeitung und der Produktion der Daten von Quelle und Ziel dieser Daten können Sie jede beliebige Kombination mischen, während Sie Ihr Programm schreiben. Künftig, wenn neue bisher nicht bekannte Formen von Quelle oder Ziel (oder Verbraucher, Verarbeiter und Erzeuger) erscheinen, können sie im gleichen Rahmen ohne Änderung von Klassen benutzt werden. Neue Stromabstraktionen, die höhere Interpretationsebenen »oberhalb« der Bytes unterstützen, können völlig unabhängig von den zugrundeliegenden Mechanismen für den Transport der Bytes geschrieben werden.

An der Spitze dieses Stromrahmens stehen die zwei abstract-Klassen InputStream und OutputStream. Wenn Sie sich das Diagramm von java.io in Anhang B ansehen, erkennen Sie, daß unter diesen Klassen eine virtuelle Fülle kategorisierter Klassen steht, die den breiten Bereich von Strömen im System, aber auch eine äußerst gut ausgelegte Hierarchie von Beziehungen zwischen diesen Strömen aufzeigt. Wir beginnen mit den Eltern und arbeiten uns durch diesen buschigen Baum.

Eingabeströme

Alle Methoden, die wir heute untersuchen, werden mit throw IOExceptions deklariert. Diese neue Subklasse von Exception verkörpert konzeptionell alle möglichen I/O-Fehler, die bei der Verwendung von Strömen auftreten können. Mehrere Subklassen davon definieren ein paar spezifischere Ausnahmen, die ebenfalls ausgeworfen werden können. Vorläufig genügt zu wissen, daß Sie eine IOException entweder mit catch auffangen oder in eine Methode stellen müssen, die sie weitergeben kann.

Die abstrakte Klasse InputStream

InputStream ist eine abstract-Klasse, die die Grundlagen für das Lesen eines Bytestroms durch den Verbraucher (das Ziel) von einer Quelle definiert. Die Identität der Quelle und die Art des Erstellens und Beförderns der Bytes ist nicht relevant. Bei der Verwendung eines Eingabestroms sind Sie das Ziel dieser Bytes. Das ist alles, was Sie wissen müssen.

read()

Die wichtigste Methode für den Verbraucher eines Eingabestroms ist die, die die Bytes von der Quelle liest. Diese Methode ist read(). Sie existiert in vielen Varianten, von denen in der heutigen Lektion je ein Beispiel aufgezeigt wird.

Jede dieser read()-Methoden ist so definiert, daß sie warten muß, bis alle angeforderten Eingaben verfügbar sind. Sorgen Sie sich nicht wegen dieser Einschränkung. Dank Multithreading können Sie viele andere Dinge realisieren, während ein Thread auf eine Eingabe wartet. Üblicherweise wird ein Thread je einem Eingabestrom (und je einem Ausgabestrom) zugewiesen, der allein für das Lesen (oder Schreiben) vom jeweiligen Strom zuständig ist. Die Eingabe-Threads können dann die Informationen zur Verarbeitung an andere Threads abgeben. Dadurch überlappt natürlich die I/O-Zeit Ihres Programms mit seiner Berechnungszeit.

Hier die erste Form von read():

InputStream s = getAnInputStreamFromSomewhere();

byte[] buffer = new byte[1024]; // Kann beliebige Größe sein

if (s.read(buffer) != buffer.length)

   System.out.println("I got less than I expected.");

Hier und in der gesamten Lektion gehen wir davon aus, daß entweder import java.io vor allen Beispielen erscheint, oder daß Sie alle Referenzen auf java.io-Klassen mit dem Präfix java.io schreiben.

Diese Form von read() versucht, den gesamten zugeteilten Puffer zu füllen. Gelingt es ihr nicht (normalerweise, weil das Ende des Eingabestroms vorher erreicht wird), gibt sie die tatsächliche Anzahl von Bytes aus, die in den Puffer eingelesen wurden. Danach gibt ein eventueller weiterer Aufruf von read() den Wert -1 aus, was anzeigt, daß Sie sich am Ende des Stroms befinden. Die if-Anweisung funktioniert auch in diesem Fall, weil -1 != 1024 ist (entspricht einem Eingabestrom ohne Bytes).

Im Gegensatz zu C wird der -1-Fall in Java nicht benutzt, um einen Fehler anzuzeigen. Eventuelle I/O-Fehler werfen Instanzen von IOException aus (was wir noch nicht mit catch auffangen). Sie haben am 17. Tag gelernt, daß alle bestimmten Werte durch Ausnahmen ersetzt werden können und sollten. -1 im letzten Beispiel ist ein historischer Anachronismus. Sie werden gleich einen besseren Ansatz zum Anzeigen des Stromendes mit der Klasse DataInputStream kennenlernen.

Sie können auch in einen Bereich Ihres Puffers einlesen, indem Sie den Versatz und die gewünschte als Argumente von read() angeben:

s.read(buffer, 100, 300);

Bei diesem Beispiel werden Bytes 100 bis 399 aufgefüllt. Andernfalls verhält es sich genauso wie mit der vorherigen read()-Methode. In der aktuellen Version benutzt die Standardimplementierung der ersten Form von read() die zweite Alternative:

public int read(byte[] buffer) throws IOException {

   return read(buffer, 0, buffer.length);

}

Außerdem können die Bytes auch einzeln eingelesen werden:

InputStream s = getAnInputStreamFromSomewhere();

byte b;

int byteOrMinus1;

while ((byteOrMinus1 = s.read()) != -1) {

b = (byte) byteOrMinus1;

... // Verarbeitung von Byte b

}

... // Lesen des Stromendes

Aufgrund der allgemeinen Natur von Ganzzahlenerhöhungen in Java und weil die read()-Methode in diesem Fall int ausgibt, kann die Verwendung des byte-Typs im Code ein bißchen frustrierend sein. Man muß ständig das Ergebnis von arithmetischen Ausdrücken oder int-Ausgabewerten in die gewünschte Größe umwandeln. Da read()in diesem Fall eigentlich byte ausgeben sollte, halte ich es für besser, die Methode als solche zu deklarieren und zu verwenden. In Fällen, in denen man das Gefühl hat, der Bereich einer Variablen ist auf byte (oder short) begrenzt, sollte man sich die Zeit nehmen, und dies nicht mit int, sondern als was es ist zu deklarieren. Nebenbei bemerkt, speichert ein Großteil des Codes der Java-Klassenbibliothek das Ergebnis von read() als int. Das zeugt von der Menschlichkeit des Java-Teams - jeder kann schließlich Fehler machen.

skip()

Für den Fall, daß Sie einige Bytes in einem Strom überspringen oder von einer anderen Stelle mit dem Lesen des Stroms beginnen wollen, gibt es eine mit read() vergleichbare Methode:

if (s.skip(1024) != 1024)

   System.out.println("I skipped less than I expected.");

Dadurch werden die nächsten 1024 Bytes des Eingabestroms übersprungen. skip() nimmt und gibt eine lange Ganzzahl, weil Ströme nicht auf eine bestimmte Größe begrenzt werden müssen. Die Standardimplementierung von skip benutzt in diesem Release einfach read():

public long skip(long n) throws IOException {

   byte[] buffer = new byte[(int) n];

   return read(buffer);

}

Diese Implementierung unterstützt große skip-Methoden nicht korrekt, weil ihr long-Argument in eine Ganzzahl (int) umgewandelt wird. In Subklassen muß diese Standardimplementierung überschrieben werden, um dies korrekt abzuarbeiten. Das ist nicht so einfach, wie Sie denken, weil das derzeitige Java-Release keine integer-Typen zuläßt, die größer sind als int.

available()

Wenn Sie wissen wollen, wie viele Bytes ein Strom momentan umfaßt, können Sie so fragen:

if (s.available() < 1024)

   System.out.println("Too little is available right now.");

Dadurch wird Ihnen die Anzahl von Bytes mitgeteilt, die ohne Blockierung mit read() gelesen werden kann. Aufgrund der abstrakten Natur der Quelle dieser Bytes sind Ströme eventuell nicht in der Lage, Ihnen auf diese Frage eine Antwort zu geben. Einige Ströme geben beispielsweise immer 0 aus. Sofern Sie keine spezifischen Subklassen von InputStream verwenden, die Ihnen eine vernünftige Antwort auf diese Frage geben, sollten Sie sich nicht auf diese Methode verlassen. Multithreading schließt ohnehin viele Probleme in Verbindung mit der Blokkierung während der Wartezeit auf einen Strom aus. Damit schwindet einer der vorrangigen Nutzen von available() dahin.

mark() und reset()

Einige Ströme unterstützen die Markierung einer Position im Strom und das spätere Zurücksetzen des Stroms auf diese Position. Der Strom müßte sich dabei an alle Bytes erinnern, deshalb gibt es eine Einschränkung, in welchem Abstand in einem Strom markiert und zurückgesetzt werden kann. Ferner gibt es eine Methode, die fragt, ob der Strom dies überhaupt unterstützt. Hier ein Beispiel:

InputStream s = getAnInputStreamFromSomewhere();

if (s.markSupported()) { // Unterstützt s dieses Konzept?

   ... // Ein Teil des Stroms wird gelesen

   s.mark(1024);

   ... // Weniger als 1024 weitere Bytes lesen

   s.reset();

   ... // Diese Bytes erneut lesen

} else {

   ... // Nein, irgendeine Alternative ausführen

}

Durch Markieren eines Stroms wird die Höchstzahl der Bytes bestimmt, die vor dem Zurücksetzen weitergegeben werden soll. Dadurch kann der Strom den Umfang seines »Bytespeichers« eingrenzen. Läuft diese Zahl durch, ohne daß ein reset() erfolgt, wird die Markierung ungültig und der Versuch, zurückzusetzen, wirft eine Ausnahme aus.

Markieren und Zurücksetzen eines Stroms ist nützlich, wenn der Stromtyp (oder der nächste Stromteil) identifiziert werden soll.

close()

Da Sie nicht wissen, welche Ressourcen ein offener Strom darstellt und wie diese Ressourcen zu behandeln sind, nachdem der Strom gelesen wurde, müssen Sie einen Strom normalerweise explizit schließen, damit er diese Ressourcen freigeben kann. Selbstverständlich kann das der Papierkorb und eine finalize-Methode ebenfalls erledigen. Es könnte aber sein, daß Sie den Strom erneut öffnen müssen, bevor die Ressourcen freigegeben werden. Da Sie hierbei mit der Außenwelt, d. h. externen Ressourcen zu tun haben, ist es ratsam, genau anzugeben, wann deren Benutzung enden soll:

InputStream s = alwaysMakesANewInputStream();

try {

   ... // s nach Herzenslust verwenden

} finally {

   s.close();

}

Gewöhnen Sie sich an die Verwendung von finally; Sie stellen damit sicher, daß Aktionen (z. B. das Schließen eines Stroms) auf jeden Fall ausgeführt werden. Selbstverständlich gehen Sie davon aus, daß der Strom immer erfolgreich erzeugt wird. Ist das nicht stets der Fall und wird zuweilen Null ausgegeben, gehen Sie so vor:

InputStream s = tryToMakeANewInputStream();

if (s != null) {

   try {

   ...

   } finally {

      s.close();

   }

}

Alle Eingabeströme stammen von der abstract-Klasse InputStream ab. Alle haben die bisher beschriebenen Methoden. Somit könnte Strom s im vorherigen Beispiel auch einen der komplexeren Eingabeströme haben, die im folgenden beschrieben werden.

ByteArrayInputStream

Durch »Umkehr»« einiger der vorherigen Beispiele würde man einen Eingabestrom aus einer Reihe von Bytes erstellen. Genau das besorgt ByteArrayInputStream:

byte[] buffer = new byte[1024],

fillWithUsefulData(buffer);

InputStream s = new ByteArrayInputStream(buffer);

Leser des neuen Stroms s sehen einen Strom mit einer Länge von 1024 Bytes, d. h. die Bytes im Array buffer. Der Constructor dieser Klasse hat wie read() einen Versatz und eine Länge:

InputStream s = new ByteArrayInputStream(buffer, 100, 300);

Hier ist der Strom 300 Bytes lang und enthält Bytes 100-399 aus dem Array buffer.

Damit haben Sie die ersten Beispiele des Erstellens von Eingabeströmen gesehen. Diese neuen Ströme werden an die einfachsten aller möglichen Datenquellen angehängt - einen Byte-Array im Speicher des lokalen Rechners.

ByteArrayInputStreams implementiert nur die Standardmethoden, wie alle Eingabeströme. Hier hat die available()-Methode aber eine bestimmte Aufgabe: Sie gibt 1024 bzw. 300 für die zwei Instanzen von ByteArrayInputStream aus, die Sie zuvor erstellt haben, weil sie genau weiß, wie viele Bytes verfügbar sind. Schließlich wird ByteArrayInputStream durch Aufrufen von reset() an den Strom- bzw. Pufferanfang zurückgesetzt, ungeachtet dessen, wo sich die Markierung befindet.

FileInputStream

Eine der häufigsten Verwendungen von Strömen und historisch die älteste ist das Anhängen von Strömen an Dateien im Dateisystem. Hier wird beispielsweise ein solcher Eingabestrom in einem Unix-System erstellt:

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

Applets, die versuchen, solche Ströme im Dateisystem zu öffnen, zu lesen oder zu schreiben, können Sicherheitsverletzungen verursachen. Erstellen Sie möglichst Applets, die überhaupt nicht von Dateien abhängen, indem Sie Server benutzen, um die gemeinsam genutzten Informationen abzulegen. Falls das nicht möglich ist, grenzen Sie die Ein-/Ausgaben Ihres Applets auf eine einzige Datei oder ein Verzeichnis ein, das der Benutzer leicht mit Zugriffsrechten belegen kann. Bei unvernetzten Programmen stellt sich dieses Problem selbstverständlich nicht.

Sie können den Strom aus einem zuvor geöffneten Dateibezeichner erstellen:

int fd = openInputFileInTraditionalUNIXWays();

InputStream s = new FileInputStream(fd);

Da dies auf einer tatsächlichen Datei mit bestimmter Länge basiert, kann der erzeugte Eingabestrom in beiden Fällen available() und skip() implementieren (wie übrigens auch ByteArrayInputStream). FileInputStream kennt darüber hinaus noch ein paar Tricks:

FileInputStream aFIS = new FileInputStream("aFileName");

int myFD = aFIS.getFD();

/* aFIS.finalize(); */ // Ruft close() auf, wenn GC automatisch aufgerufen wird

Um neue Methoden aufzurufen, müssen Sie die Stromvariable aFIS als FileInputStream-Typ deklarieren.

getFD() gibt den Bezeichner der Datei aus, auf der der Strom basiert. Die Implementierung von finalize() (einer geschützten Methode) durch FileInputStream schließt den Strom. Im Gegensatz zum vorherigen Beispiel sollten Sie eine finalize()-Methode nie direkt aufrufen. Der Papierkorb ruft sie auf, nachdem er festgestellt hat, daß der Strom nicht mehr verwendet wird. Das System schließt den Strom (irgendwann).

Sie können sich diese Lässigkeit leisten, weil Ströme, die auf Dateien basieren, nur sehr wenige Ressourcen binden. Diese Ressourcen können nicht versehentlich vor der Reinigung wiederverwendet werden. Wenn Sie auch in die Datei schreiben, müssen Sie sorgfältiger vorgehen. Durch frühzeitiges erneutes Öffnen der Datei nach dem Schreiben kann sich ein inkonsistenter Zustand ergeben, weil finalize() und damit close() noch nicht ausgeführt wurden. Wenn Sie den Typ von InputStream nicht genau kennen, rufen Sie am besten close() selbst auf.

FilterInputStream

Diese abstrakte Klasse bietet einen »Durchlauf« für alle Standardmethoden von InputStream. Sie selbst enthält einen anderen Strom weiter unten in der Filterkette, an den sie alle Methodenaufrufe abgibt. Sie implementiert nichts Neues, gestattet aber ihr eigenes Verschachteln:

InputStream s = getAnInputStreamFromSomewhere();

FilterInputStream s1 = new FilterInputStream(s);

FilterInputStream s2 = new FilterInputStream(s1);

FilterInputStream s3 = new FilterInputStream(s2);

... s3.read() ...

Wenn read auf den gefilterten Strom s3 ausgeführt wird, wird die Anfrage an s2 weitergegeben. Dann reagiert s2 genau so wie s1, und schließlich wird s aufgefordert, die Bytes bereitzustellen. Subklassen von FilterInputStream führen eine gewisse Verarbeitung der durchfließenden Bytes aus. Die Kette im vorherigen Beispiel kann eleganter geschrieben werden:

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

Sie sollten diese Form soweit möglich immer in Ihrem Code verwenden. Sie drückt die Verschachtelung verketteter Filter deutlich aus.

Im nächsten Abschnitt betrachten wir die Subklassen von FilterInputStream.

BufferedInputStream

Das ist einer der nützlichsten Ströme. Er implementiert die vollen Fähigkeiten der InputStream-Methoden, jedoch durch Verwendung eines gepufferten Byte-Arrays, der sich als Cache für weitere Leseoperationen verhält. Dadurch werden die gelesenen »Stückchen« von den größeren Blöcken, in denen Ströme am effizientesten gelesen werden (z. B. von Peripheriegeräten, Dateien oder im Netz), abgekoppelt. Ferner ermöglicht es den Strömen, Daten vorauszulesen.

Da das Puffern von BufferedInputStream so hilfreich, und das auch die einzige Klasse ist, die mark() und reset() richtig abarbeitet, möchte man sich wünschen, daß jeder Eingabestrom diese wertvollen Fähigkeiten irgendwie nutzt. Sie haben bereits eine Möglichkeit gesehen, durch die sich Filterströme um andere Ströme »herumwickeln« können. Wenn ein gepufferter FileInputStream korrekt markieren und zurücksetzen soll, schreiben Sie folgendes:

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

Damit haben Sie einen gepufferten Eingabestrom auf der Grundlage der Datei »foo«, die mark() und reset() unterstützt.

Jetzt wird die Leistung verschachtelter Ströme langsam klar. Jede von einem gefilterten Eingabestrom (und Ausgabestrom, wie Sie bald sehen werden) bereitgestellte Fähigkeit kann durch Verschachtelung von einem anderen Strom genutzt werden. Durch Verschachtelung der Filterströme ist jede Kombination dieser Fähigkeiten in jeder Reihenfolge möglich.

DataInputStream

Alle Methoden dieser Klasse sind in einer separaten Schnittstelle definiert, die von DataInputStream und RandomAccessFile (einer weiteren Klasse in java.io) implementiert wird. Diese Schnittstelle ist allgemein, so daß Sie sie in Ihren eigenen Klassen benutzen können. Sie heißt DataInput.

Die DataInput-Schnittstelle

Wenn Sie häufigeren Gebrauch von Strömen machen, werden Sie bald feststellen, daß Byteströme kein Format bieten, in das alle Daten eingezwängt werden können. Vor allem die Primitivtypen der Java-Sprache können in den bisher behandelten Strömen nicht gelesen werden. Die DataInput-Schnittstelle spezifiziert höhere Methoden zum Lesen und Schreiben, die komplexere Datenströme unterstützen. Diese Schnittstelle definiert folgende Methoden:

void  readFully(byte[]  buffer)                         throws IOException;

void  readFully(byte[]  buffer, int offset, int length) 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;

Die ersten drei Methoden sind lediglich neue Bezeichnungen für skip() und die zwei vorher behandelten Formen von read(). Die nächsten zehn Methoden lesen einen Primitivtyp bzw. dessen vorzeichenloses Gegenstück (nützlich für die effiziente Verwendung aller Bits in einem Binärstrom). Diese Methoden müssen eine Ganzzahl mit einer breiteren Größe ausgeben. Da Ganzzahlen in Java Vorzeichen haben, passen die vorzeichenlosen Werte nicht in etwas, das kleiner ist. Die letzten zwei Methoden lesen eine neue Zeile ('\r', '\n' oder "\r\n") aus dem Strom - beendete Zeichenketten (die erste in ASCII und die zweite in Unicode).

Da Sie nun wissen, wie die Schnittstelle, die DataInputStream implementiert, aussieht, betrachten wir sie in Aktion:

DataInputStream s = new DataInputStream(getNumericInputStream());

long size = s.readLong(); // Anzahl der Elemente im Strom

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 & 0100000) != 0) {

         ... // Hohes Bitset, etwas Besonderes treiben

      }

      ... // anInteger und aDouble verarbeiten

   }

}

Die Klasse implementiert eine Schnittstelle für alle ihre Methoden, deshalb können Sie auch folgende Schnittstelle verwenden:

DataInput d = new DataInputStream(new FileInputStream("anything"));

String line;

while ((line = d.readLine()) != null) {

   ... // Zeile verarbeiten

}

EOFException

Auf die meisten Methoden von DataInputStream trifft folgendes zu: Wird das Ende des Stroms erreicht, werfen sie durch throw EOFException aus. Das ist sehr hilfreich, denn es ermöglicht Ihnen, alle Verwendungen von -1 in den bisherigen Beispielen besser zu schreiben:

DataInputStream s = new DataInputStream(getAnInputStreamFromSomewhere());

try {

   while (true) {

   byte b = (byte) s.readByte();

   ... // Byte b verarbeiten

   }

} catch (EOFException e) {

      ... // Stromende erreicht

}

Das funktioniert bei allen read-Methoden von DataInputStream, außer den letzten zwei.

skipBytes() bewirkt am Stromende nichts, readLine() gibt Null aus und readUTF() könnte UTFDataFormatException auswerfen, falls sie das Problem überhaupt feststellt.

LineNumberInputStream

In einem Editor oder Debugger ist die Zeilennumerierung wichtig. Um diese Fähigkeit in Ihrem Programm anzuwenden, benutzen Sie den Filterstrom LineNumberInputStream. Er kann sich sogar eine Zeilennummer merken und später in mark() und reset() zurückschreiben. Sie können diese Klasse wie folgt verwenden:

LineNumberInputStream aLNIS;

aLNIS = new LineNumberInputStream(new FileInputStream("source"));

DataInputStream s = new DataInputStream(aLNIS);

String line;

while ((line = s.readLine()) != null) {

   ... // Zeile verarbeiten

   System.out.println("Did line number: " + aLNIS.getLineNumber());

}

Hier werden zwei Filterströme in FileInputStream verschachtelt. Der erste liest die Zeilen nacheinander und der zweite verfolgt die Nummern dieser Zeilen. Der zwischengeschaltete Filterstrom aLNIS muß explizit benannt werden. Andernfalls können Sie später getLineNumber() nicht aufrufen. Wenn Sie die Reihenfolge der verschachtelten Ströme umkehren, kann LineNumberInputStream beim Lesen von DataInputStream die Zeilen nicht »sehen«.

Filterströme müssen quasi als »Überwacher« in der Mitte der Kette stehen und die Daten aus dem äußersten Filterstrom herausziehen, damit die Daten alle Überwacher nacheinander durchlaufen. Auch das Puffern sollte möglichst im Zentrum der Kette stattfinden. Im folgenden Beispiel sehen Sie eine unsinnige Reihenfolge:

new BufferedInputStream(new LineNumberInputStream(

   ƒnew DataInputStream(new FileInputStream("foo"));

Im nächsten Beispiel sehen Sie eine viel bessere Reihenfolge:

new DataInputStream(new LineNumberInputStream(

   ƒnew BufferedInputStream(new FileInputStream("foo"));

LineNumberInputStream kann auch angewiesen werden, setLineNumber() für die seltenen Fälle zu setzen, wenn Sie mehr darüber wissen wollen.

PushbackInputStream

Die Filterstromklasse PushbackInputStream wird üblicherweise in Parsern benutzt, um ein Zeichen der Eingabe (nach dem Lesen) »zurückzuschieben«, während versucht wird, die nächste Aktion zu ermitteln. Das ist eine vereinfachte Version von mark() und reset(). Sie erweitert die InputStream-Standardmethoden um unread(). Wie Sie sich denken können, gibt diese Methode vor, das in ihr durchgereichte Byte nie gelesen zu haben. Dann gibt sie dieses Byte als Ausgabewert an das nächste read() weiter.

Das folgende Beispiel ist eine einfache Implementierung von readLine() anhand dieser Klasse:

public class SimpleLineReader {

   private FilterInputStream s;

   public SimpleLineReader(InputStream anIS) {

      s = new DataInputStream(anIS);

   }

   ... // Andere read()-Methoden, die Strom s verwenden

   public String readLine() throws IOException {

      char[] buffer = new char[100];

      int offset = 0;

      byte thisByte;

      try {

loop: while (offset < buffer.length) {

         switch (thisByte = (byte) s.read()) {

            case '\n':

               break loop;

            case '\r':

               byte nextByte = (byte) s.read();

               if (nextByte != '\n') {

                  if (!(s instanceof PushbackInputStream)) {

                     s = new PushbackInputStream(s);

                  }

                  ((PushbackInputStream) s).unread(nextByte);

               }

               break loop;

            default:

               buffer[offset++] = (char) thisByte;

               break;

         }

      }

   } catch (EOFException e) {

      if (offset == 0)

         return null;

   }

   return String.copyValueOf(buffer, 0, offset);

   }

}

Das zeigt verschiedene Dinge auf. In diesem Beispiel ist readLine() auf das Lesen der ersten 100 Zeichen der Zeilen begrenzt. In dieser Hinsicht wird aufgezeigt, wie ein allgemeiner Zeilenverarbeiter nicht geschrieben werden sollte (wir wollen ja Zeilen in jeder Größe lesen). Außerdem werden wir daran erinnert, daß wir mit break aus einer äußeren Schleife ausbrechen können und wie ein String von einem Zeichen-Array erzeugt wird. In diesem Beispiel wird auch read() von InputStream zum Lesen der einzelnen Bytes verwendet. Das Stromende wird durch Einbinden in DataInputStream und catch EOFException festgelegt.

Ein ungewöhnlicher Aspekt ist bei diesem Beispiel die Art, wie PushbackInputStream verwendet wird. Um sicher zu sein, daß '\n' nach '\r' ignoriert wird, muß ein Zeichen vorausgelesen werden. Ist das kein '\n', muß dieses Zeichen zurückgeschoben werden. Die nächsten zwei Zeilen betrachten wir, als ob wir nicht viel über Strom s wüßten. Die allgemein angewandte Technik ist instruktiv. Erstens sehen wir, ob s bereits eine instanceof einer Art PushbackInputStream ist. Trifft das zu, können wir ihn direkt verwenden. Andernfalls wird der aktuelle Strom (egal welcher) in einen neuen PushbackInputStream gesetzt, und dieser Strom wird verwendet.

Die nachfolgende Zeile möchte die Methode unread() aufrufen. FilterInputStream von s ist aber ein Kompilierzeittyp und versteht somit diese Methode nicht. Die zwei vorherigen Zeilen gewährleisten jedoch, daß der Laufzeittyp des Stroms in s PushbackInputStream ist, so daß Sie ihn problemlos in diesen Typ umwandeln und unread() aufrufen können.

Dieses Beispiel ist aus Demonstrationszwecken etwas ungewöhnlich ausgefallen. Sie könnten auch eine PushbackInputStream-Variable deklarieren und darin DataInputStream einbinden. Umgekehrt könnte der Constructor von SimpleLineReader prüfen, ob sein Argument immer von der richtigen Klasse ist, wie PushbackInputStream das macht, bevor ein neuer DataInputStream erstellt wird. Interessant an diesem Ansatz ist das Einbinden einer Klasse bei Bedarf. Das ist bei jedem InputStream möglich und erfordert keinen zusätzlichen Aufwand. Beide Ansätze gelten als gute Designprinzipien.

Bisher wurden noch nicht alle Subklassen von FilterInputStream beschrieben. Nun ist es an der Zeit, zu den direkten Subklassen von InputStream zurückzukehren.

PipedInputStream

Diese Klasse und ihre Schwester PipedOutputStream werden später in der heutigen Lektion behandelt. Vorläufig genügt zu wissen, daß sie zusammen eine einfache zweiwegige Kommunikation zwischen Threads ermöglichen.

SequenceInputStream

Soll aus zwei Strömen ein zusammengesetzter Strom gebildet werden, wird SequenceInputStream verwendet:

InputStream s1 = new FileInputStream("theFirstPart");

InputStream s2 = new FileInputStream("theRest");

InputStream s = new SequenceInputStream(s1, s2);

... s.read() ... // Liest abwechselnd von beiden Strömen

Wir hätten das gleiche Ergebnis durch abwechselndes Lesen der Dateien auch »simulieren« können. Was aber, wenn wir den zusammengesetzten Strom s an eine andere Methode abgeben wollen, die nur einen InputStream erwartet?

Hier ein Beispiel (mit s), bei dem die Zeilen der zwei vorherigen Dateien durch ein übliches Numerierungsschema numeriert werden:

LineNumberInputStream aLNIS = new LineNumberInputStream(s);

... aLNIS.getLineNumber() ...

Diese Art der Verkettung von Strömen ist besonders nützlich, wenn Länge und Herkunft der Ströme nicht bekannt sind.

Wenn Sie mehr als zwei Ströme verketten wollen, versuchen Sie es so:

Vector v = new Vector();

... // Alle Ströme einrichten und jeden zum Vektor hinzufügen

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 Vektor ist ein Objekt-Array, das wächst, ausgefüllt, mit elementAt() aufgegriffen und aufgelistet werden kann.

Viel einfacher ist aber die Verwendung eines anderen Constructors, der SequenceInputStream bietet:

InputStream s = new SequenceInputStream(v.elements());

Sie müssen alle Sequenzen, die kombiniert werden sollen, auflisten, so daß ein einzelner Strom ausgegeben wird, der die Daten aller Sequenzen nacheinander liest.

StringBufferInputStream

StringBufferInputStream ist genau wie ByteArrayInputStream, basiert aber nicht auf einem Byte-Array, sondern auf einer Zeichenkette:

String buffer = "Now is the time for all good men to come...";

InputStream s = new StringBufferInputStream(buffer);

Alle Kommentare zu ByteArrayInputStream treffen auch hier zu (siehe ersten Abschnitt über diese Klasse).

Die Bezeichnung von StringBufferInputStream ist nicht gut gelungen, weil dieser Eingabestrom eigentlich auf einer Zeichenkette basiert. Ich hätte ihn StringInputStream nennen sollen.

Ausgabeströme

Ausgabeströme werden fast ausnahmslos mit einem brüderlichen InputStream gepaart. Führt ein InputStream eine bestimmte Operation aus, wird die umgekehrte Operation vom OutputStream ausgeführt. Was das bedeuten soll, sehen Sie in Kürze.

Die abstrakte Klasse OutputStream

OutputStream ist die abstract-Klasse, die die grundlegenden Arten definiert, in der eine Quelle (Erzeuger) einen Bytestrom in ein Ziel schreiben kann. Die Identität des Ziels und die Art der Beförderung und Speicherung der Bytes sind nicht relevant. Bei der Verwendung eines Ausgabestroms sind Sie die Quelle der Bytes. Das ist alles, was Sie wissen müssen.

write()

Die wichtigste Methode für den Erzeuger eines Ausgabestroms ist diejenige, die Bytes in das Ziel schreibt. Diese Methode ist write(), die es in verschiedenen Varianten gibt, wie Sie in den folgenden Beispielen sehen werden.

Alle Varianten der write()-Methode müssen warten, bis die gesamte angeforderte Ausgabe geschrieben ist. Diese Einschränkung soll Sie aber nicht beunruhigen. Wenn Sie sich nicht erinnern, warum das so ist, lesen Sie die Anmerkung unter read() von InputStream nach.

OutputStream s = getAnOutputStreamFromSomewhere();

byte[] buffer = new byte[1024]; // Jede Größe ist zulässig

fillInData(buffer); // Die Daten, die ausgegeben werden sollen

s.write(buffer);

Sie können auch ein »Scheibchen« Ihres Puffers schreiben, indem Sie den Versatz und die gewünschte Länge als Argumente für write() angeben:

s.write(buffer, 100, 300);

Dadurch werden Bytes 100 bis 399 ausgegeben. Ansonsten ist das Verhalten genau so wie bei der vorherigen write()-Methode. Im derzeitigen Release benutzt die Standardimplementierung der ersten Form von write() die zweite Alternative:

public void write[byte[] buffer) throws IOException {

   write(buffer, 0, buffer.length);

}

Außerdem können Sie Bytes auch einzeln ausgeben:

while (thereAreMoreBytesToOutput()) {

   byte b = getNextByteForOutput();

   s.write(b);

}

flush()

Da wir nicht wissen, mit was ein Ausgabestrom verbunden ist, können wir mit flush() die »Spülung« der Ausgabe durch einen gepufferten Cache anfordern, um sie (zeitgerecht oder überhaupt) zu erhalten. Die OutputStream-Version dieser Methode bewirkt nichts. Von ihr wird lediglich erwartet, diese Version durch Subklassen, die flush voraussetzen (z. B. BufferedOutputStream und PrintStream) mit nichttrivialen Aktionen zu überschreiben.

close()

Wie bei InputStream sollte ein OutputStream normalerweise explizit geschlossen werden, damit die von ihm beanspruchten Ressourcen freigegeben werden. Im übrigen trifft alles zu, was über InputStream gesagt wurde.

Alle Ausgabeströme stammen von der abstract-Klasse OutputStream ab und haben die oben beschriebenen Methoden.

ByteArrayOutputStream

Das Gegenstück von ByteArrayInputStream, das einen Eingabestrom für ein Byte-Array erzeugt, ist ByteArrayOutputStream, der einen Ausgabestrom an ein Byte-Array weitergibt:

OutputStream s = new ByteArrayOutputStream();

s.write(123);

...

Die Größe des (internen) Byte-Arrays wächst nach Bedarf, um einen Strom jeder beliebigen Länge zu speichern. Sie können auf Wunsch eine Anfangskapazität festlegen:

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

Damit haben Sie die ersten Beispiele des Erstellens von Eingabeströmen gesehen. Diese neuen Ströme werden an die einfachsten aller möglichen Datenquellen angehängt - einen Byte-Array im Speicher des lokalen Rechners.

Nachdem ByteArrayOutputStream s gefüllt wurde, kann er Daten an einen anderen Ausgabestrom ausgeben:

OutputStream anotherOutputStream = getTheOtherOutputStream();

ByteArrayOutputStream s = new ByteArrayOutputStream();

fillWithUsefulData(s);

s.writeTo(anotherOutputStream);

Außerdem kann er als Byte-Array herausgezogen oder in eine Zeichenkette konvertiert werden:

byte[] buffer = s.toByteArray();

String bufferString = s.toString();

String bufferUnicodeString = s.toString(upperByteValue);

Die letzte Methode ermöglicht das »Simulieren« von Unicode-Zeichen (16 Bit) durch Auffüllen der niedrigen Bytes mit ASCII und Spezifizieren eines oberen Bytes (normalerweise 0), um eine Unicode-Zeichenkette zu erzeugen.

ByteArrayOutputStream hat zwei Utility-Methoden: Eine gibt die aktuelle Anzahl der im internen Byte-Array gespeicherten Bytes aus, die andere setzt den Array zurück, so daß der Strom von Anfang an erneut geschrieben werden kann:

int sizeOfMyByteArray = s.size();

s.reset(); // s.size() würde jetzt 0 ausgeben

s.write(123);

...

FileOutputStream

Eine der häufigsten Verwendungen von Strömen und historisch die älteste ist das Anhängen von Strömen an Dateien im Dateisystem. Hier wird beispielsweise ein solcher Ausgabestrom in einem Unix-System erstellt:

OutputStream s = new FileOutputStream("/some/path/and/fileName");

Applets, die versuchen, solche Ströme im Dateisystem zu öffnen, zu lesen oder zu schreiben, können Sicherheitsverletzungen verursachen. Erstellen Sie möglichst Applets, die überhaupt nicht von Dateien abhängen, indem Sie Server benutzen, um die gemeinsam genutzten Informationen abzulegen. Falls das nicht möglich ist, grenzen Sie die Ein-/Ausgaben Ihres Applets auf eine einzige Datei oder ein Verzeichnis ein, das der Benutzer leicht mit Zugriffsrechten belegen kann. Bei unvernetzten Programmen stellt sich dieses Problem selbstverständlich nicht.

Sie können den Strom aus einem zuvor geöffneten Dateibezeichner erstellen:

int fd = openOutputFileInTraditionalUNIXWays();

OutputStream s = new FileOutputStream(fd);

FileOutputStream ist das Gegenstück von FileInputStream und kennt die gleichen Tricks:

FileOutputStream aFOS = new FileOutputStream("aFileName");

int myFD = aFOS.getFD();

/* aFOS.finalize(); */ // Ruft close() auf, wenn GC automatisch aufgerufen wird

Um neue Methoden aufzurufen, müssen Sie die Stromvariable aFOS als FileOutputStream-Typ deklarieren.

getFD() gibt den Bezeichner der Datei aus, auf der der Strom basiert. Die Implementierung von finalize() (einer geschützten Methode) in FileOutputStream schließt den Strom (siehe weitere Anmerkungen unter FileInputStream).

FilterOutputStream

Diese abstrakte Klasse bietet einen »Durchlauf« für alle Standardmethoden von OutputStream. Sie selbst enthält einen anderen Strom weiter unten in der Filterkette, an den sie alle Methodenaufrufe abgibt. Sie implementiert nichts Neues, gestattet aber ihr eigenes Verschachteln:

OutputStream s = getAnOutputStreamFromSomewhere();

FilterOutputStream s1 = new FilterOutputStream(s);

FilterOutputStream s2 = new FilterOutputStream(s1);

FilterOutputStream s3 = new FilterOutputStream(s2);

... s3.write(123) ...

Wenn write auf den gefilterten Strom s3 angewandt wird, wird die Anfrage an s2 weitergegeben. Dann reagiert s2 genau so wie s1, und schließlich wird s aufgefordert, die Bytes auszugeben. Subklassen von FilterOutputStream führen eine gewisse Verarbeitung der durchfließenden Bytes aus. Die Kette kann noch straffer verschachtelt werden (siehe unter FilterInputStream).

Im nächsten Abschnitt betrachten wir die Subklassen von FilterOutputStream.

BufferedOutputStream

Das ist einer der nützlichsten Ströme. Er implementiert die vollen Fähigkeiten der OutputStream-Methoden, jedoch durch Verwendung eines gepufferten Byte-Arrays, der sich als Cache für Schreiboperationen verhält. Dadurch werden die geschriebenen Teile von den größeren Blöcken, in denen Ströme am effizientesten geschrieben werden (z. B. Peripheriegeräte, Dateien oder im Netz), abgekoppelt.

BufferedOutputStream ist eine von zwei Klassen der Java-Bibliothek, die flush() implementieren. Sie können jeden Ausgabestrom so umgeben, daß folgendes erreicht wird:

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

Damit haben Sie einen gepufferten Ausgabestrom auf der Grundlage der Datei »Foo«, die flush() unterstützt.

Jede von einem gefilterten Ausgabestrom bereitgestellte Fähigkeit kann durch Verschachteln von einem anderen Strom genutzt werden. Durch Verschachteln der Filterströme ist jede Kombination dieser Fähigkeiten in jeder Reihenfolge möglich.

DataOutputStream

Alle Methoden dieser Klasse sind in einer separaten Schnittstelle definiert, die von DataOutputStream und RandomAccessFile (einer weiteren Klasse in java.io) implementiert wird. Diese Schnittstelle ist allgemein, so daß Sie sie in Ihren eigenen Klassen benutzen können. Sie heißt DataOutput.

Die DataOutput-Schnittstelle

In Zusammenhang mit dem Gegenstück DataInput bietet DataOutput höhere Methoden zum Lesen und Schreiben von Daten. Diese Schnittstelle definiert folgende Methoden:

void  write(int i)                                   throws IOException;

void  write(byte[]  buffer, int offset, int length)  throws IOException;

void  write(byte[]  buffer, int offset, int length)  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;

Zu den meisten dieser Methoden gibt es DataInput-Gegenstücke.

Die ersten drei Methoden entsprechen den drei vorher aufgezeigten write()-Methoden. Die nächsten acht Methoden geben einen Primitivtyp aus. Die letzten drei Methoden schreiben eine Zeichenkette in den Strom - die erste als 8-Bit-Bytes, die zweite als Unicode-Zeichen (16 Bit) und die dritte als speziellen Unicode-Strom (der von readUTF() in DataInput gelesen werden kann).

Die vorzeichenlosen Lesemethoden von DataInput haben keine DataOutput-Gegenstücke. Sie können die erforderlichen Daten über die Vorzeichenmethoden von DataOutput ausgeben, weil sie int-Argumente akzeptieren und auch die richtige Anzahl Bits für die vorzeichenlose Ganzzahl einer bestimmten Größe schreiben. Die Methode, die diese Ganzzahl liest, muß das Vorzeichenbit richtig interpretieren.

Da Sie nun wissen, wie die Schnittstelle, die DataOutputStream implementiert, aussieht, betrachten wir sie in Aktion:

DataOutputStream s = new DataOutputStream(getNumericOutputStream());

long size = getNumberOfItemsInNumericStream();

s.writeLong(size);

for (int i = 0; i < size; ++i) {

   if (shouldProcessNumber(i)) {

      s.writeBoolean(true); // Sollte dieses Element verarbeiten

      s.writeInt(theIntegerForItemNumber(i));

      s.writeShort(theMagicBitFlagsForItemNumber(i));

      s.writeDouble(theDoubleForitemNumber(i));

   } else

      s.writeBoolean(false);

}

Das ist das genaue Gegenstück des mit DataInput aufgeführten Beispiels. Zusammen bilden sie ein Paar, das ein bestimmtes strukturiertes Primitivtypen-Array über jeden Strom (bzw. die Transportschicht) austauschen kann. Verwenden Sie dieses Paar als Sprungbrett für ähnliche Aktionen.

Zusätzlich zur obigen Schnittstelle implementiert die Klasse eine (selbsterklärende) Utility-Methode:

int theNumberOfBytesWrittenSoFar = s.size();

Verarbeiten einer Datei

Zu den häufigsten E/A-Operationen zählen das Öffnen einer Datei, das zeilenweise Lesen und Verarbeiten und das Ausgeben dieser Daten in eine andere Datei. Das folgende Beispiel ist ein Prototyp dessen, wie dies in Java realisiert wird:

DataInput aDI = new DataInputStream(new FileInputStream("source"));

DataOutput aDO = new DataOutputStream(new FileOutputStream("dest"));

String line;

while ((line = aDI.readLine()) != null) {

   StringBuffer modifiedLine = new StringBuffer(line);

   ... // modifiedLine vor Ort verarbeiten

   aDO.writeBytes(modifiedLine.toString());

}

aDI.close();

aDO.close();

Möchten Sie das byteweise verarbeiten, schreiben Sie folgendes:

try {

   while (true) {

      byte b = (byte) aDI.readByte();

   ... // b vor Ort verarbeiten

   aDO.writeByte(b);

   }

} finally {

   aDI.close();

   aDO.close();

}

Der folgende nette Zweizeiler kopiert die Datei:

try { while (true) aDO.writeByte(aDI.readByte()); }

finally { aDI.close(); aDO.close(); }

Bei zahlreichen Beispielen der heutigen Lektion (darunter die letzten zwei) wird davon ausgegangen, daß sie in einer Methode erscheinen, die IOException in ihrer throws-Klausel hat. Deshalb kümmern sie sich nicht um das catch dieser Ausnahmen.

PrintStream

Ohne sich eventuell dessen bewußt zu sein, sind Sie bereits mit den zwei Methoden der PrintStream-Klasse vertraut. Wenn Sie die Methodenaufrufe

System.out.print(...)

System.out-println(...)

verwenden, benutzen Sie eigentlich eine PrintStream-Instanz, die sich in der Variablen out der System-Klasse befindet, um die Ausgabe auszuführen. System.err gehört ebenfalls zu PrintStream und System.in ist ein InputStream.

Auf Unix-Systemen werden diese drei Ströme an Standardausgaben, Standardfehler und Standardeingaben angehängt.

PrintStream ist ein Ausgabestrom ohne brüderliches Gegenstück. Da er normalerweise mit einer Bildschirmausgabe zusammenhängt, implementiert er flush(). Ferner bietet er die bekannten Methoden close() und write() sowie eine Fülle von Möglichkeiten zur Ausgabe der Primitivtypen und Zeichenketten von Java:

public void write(int b);

public void write(byte[] buffer, int offset, int length);

public void flush();

public void close();

public void print(Object o);

public void print(String s);

public void print(char[] buffer);

public void print(char c);

public void print(int i);

public void print(long l);

public void print(float f);

public void print(double d);

public void print(boolean b);

public void println(Object o);

public void println(String s);

public void println(char[] buffer);

public void println(char c);

public void println(int i);

public void println(long l);

public void println(float f);

public void println(double d);

public void println(boolean b);

public void println(); // Eine Leerzeile ausgeben

PrintStream kann auch um jeden Ausgabestrom angeordnet werden, wie eine Filterklasse:

PrintStream s = PrintStream(new FileOutputStream("foo"));

s.println("Here's the first ine of text in the file foo.");

Ein zweites Argument für den Constructor von PrintStream ist boolesch und bestimmt, ob der Strom automatisch »flushen« soll. Im Fall von true wird nach jedem geschriebenen Zeichen (bzw. nach dem Schreiben einer Zeichengruppe bei der write()-Form mit drei Argumenten) flush() gesendet.

Das folgende kleine Programm arbeitet wie der Unix-Befehl cat. Es nimmt die Standardeingabe zeilenweise entgegen und gibt sie auf der Standardausgabe aus:

import java.io.*; // Das einzige Mal in dieser Lektion schreiben wir das

public class Cat {

   public static void main(String args[]) {

      DataInput d = new DataInputStream(System.in);

      String line;

   try { while ((line = d.readLine()) != null)

      System.out.println(line);

         } catch (IOException ignored) { }

      }

}

PipedOutputStream

Diese und die Klasse PipedInputStream bilden zusammen das Paar, das eine unixartige Pipe-Verbindung zwischen zwei Threads herstellt und sorgfältig die gesamte Synchronisation implementiert, die die sichere Operation dieser Art von gemeinsamer Warteschlange ermöglicht. Die Verbindung wird so eingerichtet:

PipedInputStream sIn = PipedInputStream();


PipedOutputStream sOut = PipedOutputStream(sIn);

Ein Thread schreibt in sOut und der andere liest von sIn. Durch Einrichten solcher Paare können die Threads in beiden Richtungen problemlos kommunizieren.

Zusammenhängende Klassen

Die übrigen Klassen und Schnittstellen in java.io ergänzen die Ströme, so daß ein komplettes E/A-System bereitgestellt wird.

Die File-Klasse abstrahiert »File« auf plattformunabhängige Weise. Mit einem Dateinamen kann sie auf Anfragen über Typ, Status und Eigenschaften einer Datei oder eines Verzeichnisses im Dateisystem reagieren.

Anhand einer Datei, eines Dateinamens oder eines Dateibezeichners wird eine RandomAccessFile erzeugt. Sie umfaßt Implementierungen von DataInput und DataOutput in einer Klasse. Zusätzlich zu diesen Schnittstellen bietet RandomAccessFile bestimmte traditionelle Einrichtungen nach Unix-Art, z. B. seek() zum Suchen eines Zufallspunkts in einer Datei.

Die StreamTokenizer-Klasse greift einen Eingabestrom und erzeugt daraus eine Folge von Token. Durch Überschreiben verschiedener darin enthaltener Methoden in den Subklassen können Sie starke lexikale Parser erstellen.

In der API-Beschreibung Ihres Java-Releases finden Sie weitere Informationen über diese Klassen.

Zusammenfassung

Heute haben Sie das allgemeine Konzept von Strömen gelernt und Beispiele mit Eingabeströmen auf der Grundlage von Byte-Arrays, Dateien, Pipes, anderen Stromfolgen und Zeichenkettenpuffern sowie Eingabefiltern, Dateneingaben, Zeilennumerierung und Zurückschieben von Zeichen durchgearbeitet.

Sie haben auch die Gegenstücke dazu - die Ausgabeströme für Byte-Arrays, Dateien, Pipes und Ausgabefilter - kennengelernt.

Sie haben sich in dieser Lektion Kenntnisse über die grundlegenden Methoden aller Ströme (z. B. read() und write()) und einige spezielle Methoden angeeignet. Sie haben das Auffangen von IOExceptions, insbesondere EOFException, gelernt.

Sie haben gelernt, mit den doppelt nützlichen DataInput- und DataOutput-Schnittstellen umzugehen, die den Kern von RandomAccessFile bilden.

Java-Ströme bieten eine starke Grundlage, auf der Sie Multithreading/Streaming-Schnittstellen der komplexesten Art entwickeln können, die in Browsern (z. B. HotJava) interpretiert werden. Die höheren Internet-Protokolle und -Dienste, für die Sie künftig Ihre Applets schreiben können, sind im Prinzip nur durch Ihre Vorstellungskraft beschränkt.


Fragen und Antworten

F: In einem früheren read()-Beispiel haben Sie meiner Meinung nach mit der Variablen byteOrMinus1 etwas Plumpes angestellt. Gibt es dafür keine bessere Art? Und falls nicht, warum haben Sie in einem späteren Abschnitt die Umwandlung empfohlen?

A: Stimmt, diese Anweisungen haben wirklich etwas Schwerfälliges an sich. Man ist versucht, statt dessen etwa folgenden Code zu schreiben:

    while ((b = (byte) s.read()) != -1) {

    ... // Byte b verarbeiten

    }

Das Problem bei dieser Kurzfassung entsteht, wenn read() den Wert 0xFF (0377) ausgibt. Da dieser Wert vor der Ausgabe ein verlängertes Vorzeichen erhält, erscheint er genau so wie der ganzzahlige Wert -1, der das Stromende bezeichnet. Nur durch Speichern dieses Wertes in einer getrennten ganzzahligen Variablen und späteres Umwandeln erreicht man das gewünschte Ergebnis. Ich habe die Umwandlung in byte aus orthogonalen Gründen empfohlen. Das Speichern ganzzahliger Werte in Variablen mit korrekter Größe entspricht immer einem guten Stil (abgesehen davon sollte read() hier eine byte-Größe ausgeben und für das Stromende eine Ausnahme auswerfen).

F: Welche Eingabeströme in java.io implementieren nun eigentlich mark(), reset() und markSupported()?

A: InputStream und seine Standardimplementierungen geben für markSupported() false aus, machen bei mark() nichts und werfen durch reset() eine Ausnahme aus. Der einzige Eingabestrom, der in der derzeitigen Version die Kennzeichnung korrekt unterstützt, ist BufferedInputStream, der diese Standards übergeht. LineNumberInputStream implementiert mark() und reset(), jedoch erfolgt im derzeitigen Release auf markSupported() keine richtige Antwort.

F: Warum sollte available() nützlich sein, wenn es manchmal die falsche Antwort ausgibt?

A: Erstens muß man zugeben, daß es bei vielen Strömen richtig reagiert. Zweitens kann seine Implementierung bei manchen Netzströmen eine spezielle Anfrage senden, um bestimmte Informationen aufzudecken, die Sie andernfalls nicht einholen können (z. B. die Größe einer durch ftp übertragenen Datei). Würden Sie einen Verlaufsbalken für das Downloading oder die Übertragung von Dateien anzeigen, gäbe available() beispielsweise die Gesamtgröße der Übertragung aus bzw. andernfalls 0, was für sie und Ihre Benutzer sichtbar wäre.

F: Können Sie mir ein gutes Beispiel für die Verwendung des DataInput/DataOutput-Paars geben?

A: Eine übliche Verwendung eines solchen Schnittstellenpaars ist, wenn sich Objekte selbst zum Speichern oder Befördern über das Netz »einpökeln«. Jedes Objekt implementiert Lese- und Schreibmethoden anhand dieser Schnittstellen, so daß sie sich selbst effektiv in einen Strom umwandeln, der später am anderen Ende als Kopie des Originalobjekts wiederhergestellt werden kann.


Copyright ©1996 Markt&Technik
Buch- und Software- Verlag GmbH
Alle Rechte vorbehalten. All rights reserved.

Schreiben Sie uns!

Previous Page TOC Index Next Page See Page