vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 17



Java und Streams

Heute lernen Sie alles über Java-Datenstreams mit folgenden Schwerpunkten:

Sie lernen auch zwei Datenstreamschnittstellen kennen, die das Lesen und Schreiben getippter Datenstreams vereinfachen. Ferner lernen Sie, wie ganze Objekte gelesen und geschrieben werden. Und Sie lernen mehrere Utility-Klassen kennen, die benutzt werden, um auf das Dateisystem zuzugreifen. Wir beginnen mit einer kurzen Geschichte über Datenstreams.

Eine der ersten Erfindungen des Unix-Betriebssystems war die Pipe. Eine Pipe ist ein nichtinterpretierter Byte-Stream, der zur Kommunikation zwischen Programmen (bzw. »gegabelten« Kopien eines Programms) oder zum Lesen und Schreiben von verschiedenen Peripheriegeräten und Dateien benutzt wird. 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 Datenstreams gipfelten.


Ein Stream oder Datenstream ist ein Kommunikationspfad zwischen der Quelle und dem Ziel eines Informationsblocks.

Dieser Informationsblock, d.h. ein nichtinterpretierter Byte-Stream, kann von jeder »Pipe-Quelle«, dem Rechnerspeicher oder auch vom Internet kommen. Quelle und Ziel eines Datenstreams sind willkürliche Erzeuger bzw. Verbraucher von Bytes. Darin liegt die Leistung dieser Abstraktion. Sie müssen beim Lesen nichts über die Quelle und beim Schreiben nichts über das Ziel des Datenstreams wissen.

Allgemeine Methoden, die von jeder beliebigen Quelle lesen können, akzeptieren ein Streamargument, das die Quelle bezeichnet. Allgemeine Methoden zum Schreiben akzeptieren einen Stream, um das Ziel zu bestimmen. Arbiträre Prozessoren (oder Filter ) haben zwei Streamargumente. 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-Datenstreams einer NASA-Sonde im Weltraum.

Durch Entkoppeln 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, Verarbeitung und Erzeuger) erscheinen, können sie im gleichen Rahmen ohne Änderung von Klassen benutzt werden. Neue Streamabstraktionen, 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.


Eine solche Interpretation der höheren Ebene hat genau das bewirkt: Die mit Java 1.1 eingeführten Streams zur Objektserialisierung werden »oberhalb« des Datenstreammechanismus von Version 1.0 geschrieben. Sie lernen noch in der heutigen Lektion mehr über die Serialisation von Objekten.

Die Bais dieses Streamgerüsts bilden die zwei abstrakten Klassen InputStream und OutputStream. Wenn Sie sich ein Hierarchiediagramm von java.io ansehen, erkennen Sie, daß unter diesen Klassen eine Fülle von Klassen steht, die den breiten Bereich von Datenstreams im System, aber auch eine äußerst gut ausgelegte Hierarchie von Beziehungen zwischen diesen Datenstreams aufzeigt. Ein ähnlicher Baum ist im Diagramm von java.io-rw vorhanden, der in den abstrakten Eltern Reader und Writer seine Wurzeln hat. Wir beginnen mit diesen Elternklassen und arbeiten uns durch diesen buschigen Baum.


Da sich jede Klasse der heutigen Lektion im Paket java.io befindet, müssen Sie die einzelnen Klassen vor der Verwendung entweder importieren (oder ihren ausgeschriebenen Namen, z.B. java.io.InputStream benutzen) oder am Beginn der Klasse eine Anweisung import java.io.* schreiben. Alle Methoden der heutigen Lektion sind so deklariert, daß sie Ausnahmen vom Typ IOExceptions auswerfen können. Diese neue Unterklasse von Exception verkörpert konzeptionell alle möglichen Ein- und Ausgabefehler, die bei der Benutzung von Datenstreams, Reader und Writer usw. in Java vorkommen können. (IOException hat viele Subklassen, die spezifischere Ausnahmen definieren, 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. (Zur Erinnerung: Mit Ausnahmen haben wir uns in der 16. Lektion beschäftigt.)

Eingabedatenstreams und Reader

Die Grundlagen für alle Eingabeoperationen von Java bilden die zwei in den nächsten Unterabschnitten beschriebenen Klassen. Nach deren Definition ergeben sich die analogen Klassen, die aus den hier dargestellten stammen, weil diese Klassenpaare fast identische Methodenschnittstellen haben und auf die gleiche Weise benutzt werden.

Die abstrakten Klassen InputStream und Reader

InputStream ist eine abstrakte Klasse, die die Grundlagen für das Lesen eines Byte- Streams durch den Verbraucher (das Ziel) von einer Quelle definiert. Die Identität der Quelle und die Art, wie die Bytes erstellt und befördert werden, ist nicht relevant. Bei der Verwendung eines Eingabestreams sind sie das Ziel dieser Bytes. Das ist alles, was Sie wissen müssen.

Reader ist eine abstrakte Klasse, die die Grundlagen definiert, wie ein Ziel (Verbraucher) einen aus Zeichen bestehenden und von irgendeiner Quelle kommenden Datenstream liest. Der Leser (Reader) und alle seine Unterklassen sind analog der Klasse InputStream und allen ihren Unterklassen, ausgenommen, daß sie als zugrundeliegende Informationseinheiten Zeichen anstelle von Bytes benutzen.

read()

Die wichtigste Methode für den Verbraucher eines Eingabestreams (oder Reader) ist die, die die Bytes (Zeichen) 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 Eingabestream (und je einem Ausgabestream) zugewiesen, der allein für das Lesen vom (oder Schreiben zum) jeweiligen Stream 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();
Reader       r       = getAReaderFromSomewhere();
byte[]       bbuffer = new byte[1024];   // Kann beliebige Größe sein
char[]       cbuffer = new char[1024];

if (s.read(bbuffer) != bbuf.length   ||   r.read(cbuffer) != cbuf.length)
    System.out.println("Ich habe weniger erhalten als erwartet.");


Sofern nicht anders angegeben, wird jede Methode in den im folgenden beschriebenen Datenstream-, Reader- und Writer-Klassen auf die gleiche Weise benutzt wie die Klasse dieses Abschnitts. So wird beispielsweise die obige read()-Methode sowohl in InputStream als auch in Reader gleich benutzt. Hier und in der gesamten Lektion gehen wir davon aus, daß entweder import java.io.* vor jedem Beispiel 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 Eingabestreams 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 zurück, was anzeigt, daß das Ende des Datenstreams erreicht ist. Die if-Anweisung funktioniert auch, wenn der Datenstream leer ist, weil -1 nie der Pufferlänge entspricht.


Im Gegensatz zu C wird der Fall -1 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 gestern gelernt, daß alle bestimmten Werte durch Ausnahmen ersetzt werden können und sollten. Im letzten Beispiel ist -1 ein historischer Anachronismus. Sie werden gleich einen besseren Ansatz zum Anzeigen des Streamendes mit der Klasse DataInputStream kennenlernen.

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

s.read(bbuffer, 100, 300);
r.read(cbuffer, 100, 300);

Bei diesem Beispiel werden die Bytes (Zeichen) 100 bis 399 gelesen. Ansonsten 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[]  b) throws IOException {        /* Von InputStream.java */
    return  read(b, 0, b.length);
}
public int  read(char[]  cbuf) throws IOException {     /* Von Reader.java */
    return  read(cbuf, 0, cbuf.length);
}

In der dritten Form können die Bytes (Zeichen) auch einzeln eingelesen werden:

InputStream  s = getAnInputStreamFromSomewhere(); 
InputStream  r = getAReaderFromSomewhere(); 
byte         b;
char         c;
int          byteOrMinus1, charOrMinus1;

while ((byteOrMinus1 = s.read()) != -1   &&   (charOrMinus1 = r.read()) != -1) {
     b = (byte) byteOrMinus1;               c = (char) charOrMinus1;
     . . .    // Verarbeite Byte b (oder Zeichen c)
}
. . .    // Datenstreamende erreicht


Aufgrund der allgemeinen Vorliebe für Integer in Java und weil die read()-Methode in diesem Fall einen int zurückgibt, kann die Verwendung des Typs byte (oder char) 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 (oder char) zurückgeben 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, char oder short begrenzt, sollte man sich die Zeit nehmen, 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 Datenstream überspringen oder von einer anderen Stelle mit dem Lesen des Datenstreams beginnen wollen, gibt es eine mit read() vergleichbare Methode:

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

Dadurch werden die nächsten 1024 Byte des Eingabestreams übersprungen. skip() nimmt und gibt einen long-Integer zurück, weil Datenstreams nicht auf eine bestimmte Größe begrenzt werden müssen. Die Standardimplementierung von skip() in InputStream benutzt einfach read():

public long  skip(long n) throws IOException {      /* Von InputStream.java */
    byte[]  data = new byte[(int) n & 0xEFFFFFFF];
    return  read(data);
}


Diese Implementierung unterstützt große skip-Methoden nicht korrekt, weil ihr long- Argument in eine Ganzzahl (int) umgewandelt wird. (Bei der Implementierung von skip() in Reader läuft das korrekt ab - lange Zeichenzahlen werden übersprungen.) In Subklassen muß diese Standardimplementierung überladen werden, damit dies richtig abgearbeitet wird. Das ist nicht so einfach, wie Sie denken, weil Java keine Ganzzahlentypen als Array-Indizes zuläßt, die größer sind als int.

available() und ready()

Wenn Sie wissen wollen, wie viele Bytes ein Datenstream momentan umfaßt (oder ob beim Leser weitere Zeichen auf Sie warten), können Sie so fragen:

if (s.available() < 1024)
    System.out.println("Momentan ist zu wenig verfügbar.");
if (r.ready() != true)
    System.out.println("Momentan sind keine Zeichen verfügbar.");

Dadurch wird Ihnen die Anzahl von Bytes mitgeteilt, die ohne Blockierung gelesen werden können (oder ob Sie irgendwelche Zeichen lesen können). Aufgrund der abstrakten Natur der Quelle dieser Bytes sind Datenstreams eventuell nicht in der Lage, Ihnen auf diese Frage eine Antwort zu geben. Einige Datenstreams geben beispielsweise immer 0 (oder false) zurück. Dieser Wert ist der Rückgabewert der Standardimplementierung von available() (oder ready()).

Sofern Sie keine spezifischen Unterklassen 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 Blockierung während der Wartezeit auf einen Datenstream aus. Damit schwindet einer der vorrangigen Nutzen von available() (oder ready()) dahin.

mark() und reset()

Einige Datenstreams unterstützen die Markierung einer Position im Datenstream und das spätere Zurücksetzen des Datenstreams auf diese Position, um die Bytes (Zeichen) ab dieser Stelle erneut zu lesen. Der Datenstream müßte sich dabei an alle Bytes (Zeichen) »erinnern«, deshalb gibt es eine Einschränkung, in welchem Abstand in einem Datenstream markiert und zurückgesetzt werden kann. Ferner gibt es eine Methode, die fragt, ob der Datenstream dies überhaupt unterstützt. Hier ein Beispiel:

InputStream  s = getAnInputStreamFromSomewhere();
Reader       r = getAReaderFromSomewhere();

if (s.markSupported()   &&   r.markSupported()) {    // Markieren 
                                                     // unterstützt?
    . . .                                            // Datenstream eine Weile 
                                                     // lesen
    s.mark(1024);    r.mark(1024);
    . . .                                            // Weniger als 1024 weitere
                                                     // Bytes (Zeichen) 
                                                     // lesen
    s.reset();       r.reset();
    . . .                                            // Wir können diese Bytes
                                                     // (Zeichen) jetzt erneut
                                                     // lesen
} else {
    . . .                                            // Nein, führe irgendeine
                                                     // Alternative aus
}

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

Markieren und Zurücksetzen eines Datenstreams ist nützlich, wenn der Streamtyp (oder der nächste Streamteil) identifiziert werden soll. Hierfür verbrauchen Sie aber einen beträchtlichen Anteil davon im Prozeß. Oft liegt das daran, daß man mehrere Parser hat, denen man den Datenstream übergeben kann. Sie verbrauchen aber eine (Ihnen unbekannte) Zahl an Bytes (Zeichen), bevor sie sich entscheiden, ob der Datenstream ihr Typ ist. Setzen Sie eine große Größe als Lesegrenze, und lassen Sie jeden Parser ablaufen, bis er entweder einen Fehler ausgibt oder die Syntaxanalyse erfolgreich beendet. Wird ein Fehler ausgegeben, setzen Sie ihn zurück, und versuchen Sie es mit dem nächsten Parser.


Die Standardimplementierung von markSupported() gibt zwar false zurück und reset() wirft eine IOException aus, aber sowohl bei InputStream als auch bei Reader macht mark() von InputStream nichts, während mark() von Reader eine IOException erzeugt. Das bricht (leider) die fast perfekte Symmetrie der beiden Klassen.

close()

Da Sie nicht wissen, welche Ressourcen ein offener Datenstream darstellt und wie diese Ressourcen zu behandeln sind, nachdem der Datenstream gelesen wurde, müssen Sie einen Datenstream normalerweise explizit schließen, damit er diese Ressourcen freigeben kann. Selbstverständlich können das der Garbage-Collector und eine Methode finalize() ebenfalls erledigen. Es könnte aber sein, daß Sie den Datenstream erneut öffnen müssen, bevor die Ressourcen dieses asynchronen Prozesses freigegeben werden. Bestenfalls ist das ärgerlich oder verwirrend. Im schlechtesten Fall entsteht ein unerwarteter, schwer auszumachender Fehler. Da Sie hierbei mit der Außenwelt, d.h. mit externen Ressourcen, zu tun haben, ist es ratsam, genau anzugeben, wann deren Benutzung enden soll:

InputStream  s = alwaysMakesANewInputStream();
Reader       r = alwaysMakesANewReader();

try {
    . . .     // Benutze s (oder r) nach Herzenslust
} finally {
    s.close();    r.close();
}

Gewöhnen Sie sich an die Verwendung von finally. Sie stellen damit sicher, daß Aktionen (z.B. das Schließen eines Datenstreams) auf jeden Fall ausgeführt werden. Selbstverständlich gehen Sie davon aus, daß der Datenstream immer erfolgreich erzeugt wird. Ist das nicht stets der Fall und wird zuweilen null zurückgegeben, gehen Sie auf Nummer Sicher:

InputStream  s = tryToMakeANewInputStream();
Reader       r = tryToMakeAReader();

if (s != null   &&   r != null) {
    try {

        . . .
    } finally {
        s.close();    r.close();
    }
}

Alle Eingabestreams stammen von der abstrakten Klasse InputStream, und alle Reader stammen von Reader ab. Alle haben die bisher beschriebenen Methoden. Somit könnte InputStream s (oder Reader r) im vorherigen Beispiel auch einen der komplexeren Eingabestreams haben, die in den nächsten Abschnitten beschrieben werden.


Konkrete Subklassen von InputStream brauchen nur die dritte Form von read() ohne Argumente zu implementieren, um alle übrigen Methoden zum Arbeiten zu bringen (InputStream hat in der Standardimplementierung die Methode close(), die nichts bewirkt). Unterklassen von Reader müssen aber sowohl close() als auch die zweite Form von read() mit den drei Argumenten implementieren.

ByteArrayInputStream und CharArrayReader

Durch »Umkehr« einiger der vorherigen Beispiele mit read() würde man einen Eingabestream (oder Reader) aus einem Byte- oder Zeichenarray erstellen. Genau das besorgt ByteArrayInputStream (bzw. CharArrayReader):

byte[]  bbuffer = new byte[1024];
char[]  cbuffer = new char[1024];

fillWithUsefulData(bbuffer);    fillWithUsefulData(cbuffer);

InputStream  s = new ByteArrayInputStream(bbuffer);
Reader       r = new CharArrayReader(cbuffer);

Reader des neuen Datenstreams s (r) sehen einen Datenstream mit einer Länge von 1024 Byte (Zeichen), d.h. den Inhalt des Arrays bbuffer (cbuffer). Der Konstruktor dieser Klasse hat wie read() einen Versatz (Offset) und eine Länge:

InputStream  s = new ByteArrayInputStream(bbuffer, 100, 300);
Reader       r = new CharArrayReader(cbuffer, 100, 300);

Hier ist der Datenstream 300 Byte (Zeichen) lang und enthält Bytes 100-399 aus dem Array bbuffer (cbuffer).


Damit haben Sie die ersten Beispiele des Erstellens von Eingabestreams gesehen. Diese neuen Datenstreams werden an die einfachsten aller möglichen Datenquellen angehängt - an ein Byte- oder Zeichenarray im Speicher des lokalen Rechners.

ByteArrayInputStream (CharArrayReader) implementiert lediglich die Standardmethoden wie alle Eingabestreams. Hier hat die Methode available() (ready()) aber eine ganz bestimmte Aufgabe: Sie gibt 1024 bzw. 300 (true und true) für die zwei Instanzen von ByteArrayInputStream (CharArrayReader) zurück, die Sie zuvor erstellt haben, weil sie genau weiß, wie viele Bytes (Zeichen) verfügbar sind. Auch markSupported() gibt true zurück. Schließlich wird der Datenstream durch Aktivieren von reset() ohne ein vorangehendes mark() an den Anfang seines Puffers zurückgesetzt.

FileInputStream und FileReader

Eine der häufigsten Verwendungen von Datenstreams und historisch die älteste ist das Anhängen von Datenstreams an Dateien im Dateisystem. Hier wird beispielsweise ein solcher Eingabestream (oder Reader) auf einem Unix-System erstellt:

InputStream  s = new FileInputStream("/Irgendein/Pfad/und/Dateiname");
Reader       r = new FileReader("/Irgendein/Pfad/und/Dateiname.utf8");


Applets, die versuchen, solche Datenstreams im Dateisystem zu öffnen, zu lesen oder zu schreiben, können in den meisten Browsern Sicherheitsverletzungen verursachen. (Bei einigen Browsern kann der Benutzer verschiedene Sicherheitsfunktionen in bezug auf das Lesen und Schreiben in Verzeichnissen einstellen.) Wie Sie gestern erfahren haben, besteht in Java 1.2 die Möglichkeit, durch das Signieren von Applets diesen den Zugriff auf das Dateisystem zu gestatten.

Sie können Datenstreams auch aus einem zuvor aktivierten FileDescriptor oder einer Datei (File) erstellen:

InputStream  s = new FileInputStream(FileDescriptor.in);  /* Standardeingabe */
Reader       r = new FileReader(FileDescriptor.in);
InputStream  s = new FileInputStream(new File("/Irgendein/Pfad/und/Dateiname"));
Reader       r = new FileReader(new File("/Irgendein/Pfad/und/Dateiname.utf8"));

Da dies auf einer tatsächlichen Datei mit einer bestimmten Länge basiert, kann der erzeugte Eingabestream (Reader) in allen drei Fällen problemlos available() (ready()) und skip() implementieren (wie übrigens auch ByteArrayInputStream und CharArrayReader ).

FileReader ist eigentlich eine triviale Unterklasse der weiteren Reader-Klasse InputStreamReader , die jeden beliebigen Eingabestream (InputStream) kapseln und ihn in einen Zeichenleser umwandeln kann. Somit besteht die Implementierung von FileReader lediglich aus der Aufforderung von InputStreamReader, (selbst) einen FileInputStream zu kapseln:

public class  FileReader extends InputStreamReader {  /* Von FileReader.java */
    public  FileReader(String  fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName))
    }
    public  FileReader(File  file) throws FileNotFoundException {
        super(new FileInputStream(file))
    }
    public  FileReader(FileDescriptor  fd) throws FileNotFoundException {
        super(new FileInputStream(fd))
    }
}

FileInputStream (nicht aber FileReader) kennt darüber hinaus noch ein paar Tricks:

FileInputStream  aFIS = new FileInputStream("EinDateiname");

FileDescriptor  myFD = aFIS.getFD();
/* aFIS.finalize(); */  // Aktiviert close(), wenn GC automatisch aufgerufen wird


Um eine getFD()-Methode aufzurufen, müssen Sie die Datenstream-Variable aFIS als FileInputStream-Typ deklarieren, weil ein einfacher Eingabestream (InputStream) von getFD() keine Ahnung hat.

Ein Aspekt ist ganz klar: getFD() gibt den Bezeichner der Datei zurück, auf der der Datenstream basiert. Der zweite Aspekt ist eine interessante Kurzform, mit der Sie beliebige FileInputStream erstellen können, ohne sich um deren spätere Schließung Gedanken machen zu müssen. Die Implementierung von finalize() (einer geschützten Methode) durch FileInputStream schließt den Datenstream. Im Gegensatz zum vorherigen Beispiel sollten Sie eine finalize()-Methode nie direkt aufrufen. Der Garbage-Collector ruft sie auf, nachdem er festgestellt hat, daß der Datenstream nicht mehr gebraucht wird. Das System schließt den Datenstream (irgendwann).

Sie können sich diese Lässigkeit leisten, weil Datenstreams, die auf Dateien basieren, nur wenige Ressourcen binden. Diese Ressourcen können nicht versehentlich vor ihrer Beseitigung durch den Garbage-Collector (entgegen den vorherigen Beispielen mit finalize() und close()) wiederverwendet werden. Selbstverständlich müssen Sie sorgfältiger vorgehen, wenn Sie auch in die Datei schreiben wollen. Durch zu frühes 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 und FilterReader

Diese »abstrakten« Klassen (in Wirklichkeit ist nur FilterReader abstrakt) bieten einen »Durchlauf« für alle Standardmethoden von InputStream (oder Reader). Sie selbst enthalten einen anderen Datenstream weiter unten in der Filterkette, an die sie alle Methodenaufrufe abgeben. Sie implementieren nichts Neues, gestatten es aber, verschachtelt zu werden:

InputStream        s  = getAnInputStreamFromSomewhere();
FilterInputStream  s1 = new FilterInputStream(s);
FilterInputStream  s2 = new FilterInputStream(s1);
FilterInputStream  s3 = new FilterInputStream(s2);

... s3.read() ...

Wenn eine Leseoperation auf den gefilterten Datenstream s3 ausgeführt wird, wird die Anfrage s2 übergeben. Dann macht s2 genau das gleiche wie s1, und schließlich wird s aufgefordert, die Bytes bereitzustellen. Unterklassen von FilterInputStream führen eine gewisse Verarbeitung der durchfließenden Bytes aus. Diese im obigen Beispiel eher umständliche »Verkettung« 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. Außerdem kann sie leicht analysiert und »laut gelesen« werden, indem man ab dem innersten Datenstream s liest, bis man den äußersten Datenstream s3 erreicht.


FilterReader wird als abstrakt deklariert, deshalb können Sie davon keine Instanzen erstellen (was wir im vorherigen Beispiel mit FilterInputStream gemacht haben). Außerdem können Sie die zwei Filtertypen nicht beliebig mischen. Bytes und Zeichen lassen sich nicht mischen; available() und ready() werden nicht korrekt übergeben. Man könnte aber etwa schreiben: new FilterReaderSubclass(new InputStreamReader(new FilterInputStream(...weitere FilterInputStreams...))).

Im nächsten Abschnitt betrachten wir die Subklassen von FilterInputStream und die jeweiligen Gegenstücke von Reader.

BufferedInputStream und BufferedReader

Das sind zwei der nützlichsten Datenstreams. Sie implementieren die vollen Fähigkeiten der Methoden von InputStream und Reader, jedoch durch Verwendung eines gepufferten Byte- bzw. Zeichenarrays, der sich als Cache für weitere Leseoperationen verhält. Dadurch werden die gelesenen »Stückchen« von den größeren Blöcken, in denen Datenstreams am effizientesten gelesen werden (z.B. von Peripheriegeräten, Dateien im Dateisystem oder im Netz), abgekoppelt. Ferner ermöglicht es den Datenstreams, Daten vorauszulesen.

Da das Puffern von BufferedInputStream (BufferedReader) so hilfreich ist, und das auch die einzigen Klassen sind, die mark() und reset() richtig abarbeiten, möchte man sich wünschen, daß jeder Eingabestream (Reader) diese wertvollen Fähigkeiten irgendwie nutzt. Normalerweise hat man kein Glück, weil diese Datenstreamklassen sie nicht implementieren. Sie haben aber bereits eine Möglichkeit gesehen, durch die sich Filterstreams um andere Datenstreams »herumwickeln« können. Wenn ein gepufferter FileInputStream (FileReader) korrekt markieren und zurücksetzen soll, schreiben Sie folgendes:

InputStream  s = new BufferedInputStream(new FileInputStream("foo"));
Reader       r = new BufferedReader(new FileReader("foo"));

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


BufferedReader ist eigentlich keine Subklasse von FilterReader, läßt sich aber wie eine solche »verschachteln«, und sie implementiert alle ihre Methoden völlig analog mit BufferedInputStream, so daß die Parallele zwischen den zwei Klassen greift.

Darüber hinaus hat BufferedReader eine spezielle Methode, um eine einzelne Zeile (die mit '\r', '\n' oder '\r\n' endet) zu lesen:

BufferedReader  r    = new BufferedReader(new FileReader("foo"));
String          line = r.readLine();  // Nächste Zeile lesen

Jetzt wird die Leistung verschachtelter Datenstreams langsam klar. Jede von einem gefilterten Datenstream bereitgestellte Fähigkeit kann durch Verschachtelung von einem anderen Datenstream genutzt werden. Selbstverständlich ist durch Verschachtelung der Filterstreams jede Kombination dieser Fähigkeiten in jeder beliebigen 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äufigen Gebrauch von Datenstreams machen, werden Sie bald feststellen, daß Byte-Streams kein Format bieten, in das alle Daten eingezwängt werden können. Vor allem die primitiven Typen der Java-Sprache können in den bisher behandelten Datenstreams nicht gelesen werden. Die DataInput-Schnittstelle spezifiziert Methoden einer höheren Ebene zum Lesen und Schreiben, die komplexere Datenstreams unterstützen. Diese Schnittstelle definiert folgende Methoden:

void  readFully(byte[]  bbuffer)                           throws IOException;
void  readFully(byte[]  bbuffer, 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 IOEception;

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ärstream). Diese Methoden müssen eine Ganzzahl mit einer breiteren Größe ausgeben. Da Ganzzahlen in Java Vorzeichen haben, passen die vorzeichenlosen Werte nicht in kleinere Typen. Die letzten zwei Methoden lesen eine neue Zeile ('\r', '\n' oder '\r\n') aus dem Datenstream - beendete Zeichenketten (die erste in ASCII und die zweite in Unicode UTF-8).

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

DataInputStream  s = new DataInputStream(getNumericInputStream());

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

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) {
            . . .    // Das High-Bit ist gesetzt, etwas Besonderes damit anfangen
        }
        . . .    // Verarbeite anInteger und aDouble
    }
}

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

DataInput  d = new DataInputStream(new FileInputStream("Irgendwas"));
String     line;

while ((line = d.readLine()) != null) {
    . . .     // Verarbeite die Zeile
}

EOFException

Auf die meisten Methoden von DataInputStream trifft folgendes zu: Wird das Ende des Datenstreams erreicht, werfen sie 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();
        . . .    // Verarbeite Byte b
    }
} catch (EOFException e) {
    . . .    // Datenstreamende erreicht
}

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


skipBytes() bewirkt am Streamende nichts (außer vielleicht ein paar nutzlosen Schleifen), readLine() gibt null zurück, und readUTF() könnte UTFDataFormatException auswerfen, falls sie das Problem überhaupt feststellt. (Diese drei Methoden sind nicht die besten Beispiele für gut geschriebenen Java-Code.)

PushbackInputStream und PushbackReader

Die Filterstreamklasse PushbackInputStream (bzw. PushbackReader) wird üblicherweise in Parsern benutzt, um ein einzelnes Byte (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 (Zeichen) nie gelesen zu haben. Dann übergibt sie dieses Byte (Zeichen) dem nächsten read() als Ausgabewert. In Version 1.1 sind neue Methoden zum »Zurücklesen« (unread()) eines ganzen Puffers und eines Teilbereichs. Das bedeutet, daß es jetzt drei Formen von unread() gibt, die den drei Standardformen von read() entsprechen.

Das folgende Beispiel ist eine einfache Implementierung von readLine() anhand dieser Klasse (eine Anpassung der Implementierung aus DataInputStream.java):

public class  SimpleLineReader {
    private FilterInputStream  s;

    public  SimpleLineReader(InputStream  anIS) {
        s = new DataInputStream(anIS);
    }

    . . .    // Weitere read()-Methoden mit Datenstream s

    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 eine allgemeine Zeilenverarbeitung nicht geschrieben werden sollte - wir wollen ja Zeilen jeder Größe lesen.) Außerdem werden wir daran erinnert, daß wir mit break aus einer äußeren Schleife ausbrechen können und wie eine Zeichenkette (string) aus einem Zeichenarray erzeugt wird (in diesem Fall aus einer »Scheibe« des Zeichenarrays). In diesem Beispiel wird auch die Standardverwendung von read() in InputStream zum Lesen der einzelnen Bytes aufgezeigt. Das Stream-Ende 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 dieses Zeichen kein '\n', muß es zurückgeschoben werden. Wir betrachten die Quellzeilen, beginnend mit if (...instanceof...), als ob wir nichts über den Datenstream s wüßten. Die allgemein angewandte Technik ist lehrreich. Erstens sehen wir, ob s bereits eine Instanz (instanceof) der einen oder anderen Art von PushbackInputStream ist. Trifft das zu, können wir ihn direkt verwenden. Andernfalls wird der aktuelle Datenstream (egal welcher) in einen neuen PushbackInputStream gesetzt, und dieser neue Datenstream wird verwendet.

Die nächste Zeile möchte die Methode unread() aufrufen. Das Problem dabei ist, daß s den Kompilierzeittyp FilterInputStream hat, den somit diese Methode nicht versteht. Die zwei vorherigen Zeilen gewährleisten jedoch, daß PushbackInputStream der Laufzeittyp des Datenstreams in s 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 Konstruktor von SimpleLineReader prüfen, ob sein Argument bereits 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 Unterklassen von InputStream zurückzukehren.

ObjectInputStream

Nachdem Sie ein komplexes Geflecht aus untereinander verbundenen Objekten erstellt haben, ist oft die Möglichkeit nützlich, den Zustand all dieser Objekte gleichzeitig »speichern« zu können. Das vereinfacht das Kopieren zu Zwecken wie Backup oder Rückgängigmachen und Wiederherstellen. Außerdem bleiben die Objekte im Dateisystem erhalten und können später wieder »zum Leben erweckt« werden. Darüber hinaus können Objekte im Internet gemeinsam »auf die Reise gehen« und sicher am anderen Ende ankommen. (Sie können bereits ganze Klassen auf diese Weise senden und somit neue Instanzen am anderen Ende erstellen. Möchten Sie aber den Inhalt eines lokalen Objekts übertragen, brauchen Sie etwas Neues.)

Das JDK 1.1 führte das Konzept der Serialisation ein. Das bedeutet im wesentlichen, daß ein Objekt leicht und sicher in einen Datenstream und wieder zurück verwandelt werden kann. In Verbindung mit seiner »Bruderklasse« ObjectOutputStream bewirkt ObjectInputStream genau das.


Seit JDK 1.2 beta 3 haben Output Streams defaultmäßig ein neues Format, das nicht kompatibel mit dem alten ist. Um die Rückwärtskompatibilität zu gewährleisten gibt es die neue Funktion java.io.ObjectOutputStream.useProtocolVersion(). Verwenden Sie für das alte Format java.io.ObjectStreamConstants.PROTOCOL_VERSION_1, für das neue Format java.io.ObjectStreamConstants.PROTOCOL_VERSION_2.

Die Input Streams erkennen und benutzen automatisch das richtige Format.

Aus Sicherheitsgründen werden zur Serialisation nur Objekte zugelassen, die zum Austausch zwischen Systemen als »sicher« deklariert wurden. Solche Objekte sind Instanzen von Klassen, die die neue Schnittstelle Serializable implementieren. Die meisten internen Systemklassen implementieren Serializable nicht, wohl aber viele der mehr »informativen« Klassen.

Alle Methoden, die Instanzen dieser Klasse verstehen, sind in der getrennten Schnittstelle ObjectInput definiert, die ObjectInputStream implementiert.


Außerdem gibt es eine Schnittstelle namens Externalizable, die von Serializable abgeleitet ist. Sie gibt Objekten mehr Kontrolle darüber, wie sie geschrieben und gelesen werden. Diese Einrichtungen der unteren Ebene braucht man aber fast nie.

Die ObjectInput-Schnittstelle

Die Schnittstelle ObjectInput leitet die Schnittstelle DataInput ab, wobei sie alle ihre Methoden erbt und darüber hinaus eine neue Methode der oberen Ebene bereitstellt, die einen komplexen Typenstream serialisierter Objektdaten unterstützt:

Object  readObject() throws ClassNotFoundException, IOException;

Im folgenden einfachen Beispiel wird ein solcher Datenstream gelesen, der im »Bruderbeispiel« (ObjectOutputStream) in einem späteren Beispiel der heutigen Lektion produziert wird:

FileInputStream    s   = new FileInputStream("objectFileName");
ObjectInputStream  ois = new ObjectInputStream(s);

int      i     = ois.readInt();               // Benutzt die DataInput-Methode
String   today = (String) ois.readObject();
Date     date  = (Date)   ois.readObject();
s.close();


Denken Sie daran, daß Sie die Ergebnisse von readObject() vor Verwendung in die erwartete Klasse immer konvertieren müssen. Sogar Arrays werden als Objekte gesendet und müssen vor Verwendung in die richtigen Kompilierzeittypen konvertiert werden.


Daneben gibt es viele weitere nützliche Anwendungen der Serialisation (beispielsweise können Klassen im Datenstream enthalten sein, sich automatisch Versionen zuweisen usw.). Lesen Sie die Kommentare zu diesen Klassen oder die Dokumentation des JDK 1.2.

PipedInputStream und PipedReader

Diese Klassen und ihre Schwestern PipedOutputStream und PipedReader 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 Datenstreams ein zusammengesetzter Datenstream gebildet werden, wird SequenceInputStream verwendet:

InputStream  s1 = new FileInputStream("theFirstPart");
InputStream  s2 = new FileInputStream("theRest");

InputStream  s  = new SequenceInputStream(s1, s2);

... s.read() ...   // Liest nacheinander aus jedem Datenstream

Wir hätten das gleiche Ergebnis durch abwechselndes Lesen der Dateien auch »simulieren« können. Was aber, wenn wir den zusammengesetzten Datenstream s einer anderen Methode übergeben 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 Datenstreams ist besonders nützlich, wenn Länge und Herkunft der Datenstreams nicht bekannt sind.

Wenn Sie mehr als zwei Datenstreams verketten wollen, versuchen Sie es so:

Vector  v = new Vector();
. . .   // Setze alle Datenstreams und füge jeden einzelnen zum Vektor hinzu
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 (ein Objekt der Klasse Vector) ist ein Objektarray, das dynamisch seine Größe verändern kann, dem Elemente hinzugefügt werden können, das mit elementAt() einzelne Elemente ansprechen und dessen Inhalt aufgelistet werden kann.

Viel einfacher ist aber die Verwendung eines anderen Konstruktors, den SequenceInputStream bietet:

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

Hierfür ist eine Aufzählung aller Datenstreams erforderlich, die kombiniert werden sollen. Im Anschluß wird ein einzelner Datenstream zurückgegeben, der die Daten nacheinander liest.

StringBufferInputStream und StringReader

StringBufferInputStream (StringReader) ist genau wie ByteArrayInputStream (CharArrayReader), basiert aber nicht auf einem Byte- bzw. Zeichenarray, sondern auf einem String:

String       buffer = "Now is the time for all good men to come...";
InputStream  s      = new StringBufferInputStream(buffer);
Reader       r      = new StringReader(buffer);

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


Die Bezeichnung StringBufferInputStream ist nicht gut gelungen, weil dieser Eingabestream eigentlich auf einem String basiert. StringInputStream wäre besser geeignet. Außerdem handhabt er reset() durch Zurücksetzen der Zeichenkette an den Anfang, und markSupported() gibt false zurück (ist also nicht ganz symmetrisch mit StringReader). Das sind im wesentlichen Bugs, die aus Version 1.0 stammen.

Ausgabedatenstreams und Writer

Ausgabedatenstreams und Writer werden fast ausnahmslos mit einem brüderlichen InputStream (bzw. Reader) gepaart. Führt ein InputStream (Reader) eine bestimmte Operation aus, wird die umgekehrte Operation vom OutputStream (Writer) ausgeführt. Was das bedeuten soll, sehen Sie in Kürze.

Die abstrakten Klassen OutputStream und Writer

OutputStream ist die abstrakte Klasse, die die grundlegenden Arten definiert, in der eine Quelle (Erzeuger) einen Bytestream 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 Ausgabestreams sind Sie die Quelle der Bytes. Das ist alles, was Sie wissen müssen.

Writer ist eine abstrakte Klasse, die die grundlegenden Arten definiert, in der eine Quelle (Erzeuger) einen Bytestream in ein Ziel schreiben kann. Sie und alle ihre Unterklassen entsprechen OutputStream und ihren Unterklassen, ausgenommen, daß sie als Grundeinheiten nicht Bytes, sondern Zeichen verwenden.

write()

Die wichtigste Methode für den Erzeuger eines Ausgabestreams (oder Writers) ist diejenige, die Bytes (Zeichen) 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 den Hinweis zu read() unter InputStream .

OutputStream  s       = getAnOutputStreamFromSomewhere();
Writer        w       = getAWriterFromSomewhere();
byte[]        bbuffer = new byte[1024];         // Größe kann beliebig sein
char[]        cbuffer = new byte[1024];

fillInData(bbuffer);    fillInData(cbuffer);    // Die Daten, die wir ausgeben
                                                // wollen
s.write(bbuffer);
w.write(cbuffer);

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(bbuffer, 100, 300);
w.write(cbuffer, 100, 300);

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

public void  write(byte[]  b) throws IOException {   /* Von OutputStream.java */
    write(b, 0, b.length);
}
public void  write(char[]  cbuf) throws IOException {      /* Von Writer.java */
    write(cbuf, 0, cbuf.length);
}

Letztlich können Sie Bytes einzeln ausgeben:

while (thereAreMoreBytesToOutput()   &&   thereAreMoreCharsToOutput()) {
    byte  b = getNextByteForOutput();
    char  c = getNextCharForOutput();

    s.write(b);
    w.write(c);
}


Writer hat eigentlich zwei zusätzliche Methoden zum Schreiben einer Zeichenkette (string) und einer Zelle einer Zeichenkette. Sie werden genauso benutzt wie die ersten zwei Formen von write() (lediglich cbuffer wird durch string ersetzt).

flush()

Da wir nicht wissen, womit ein Ausgabestream (Writer) verbunden ist, können wir mit flush() die Leerung 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 (oder Reader) sollte ein OutputStream normalerweise explizit geschlossen werden, damit die von ihm beanspruchten Ressourcen freigegeben werden. Im übrigen trifft alles zu, was über close() in Zusammenhang mit InputStream gesagt wurde.

Alle Ausgabestreams stammen von der abstrakten Klasse OutputStream, und alle Writer stammen von Writer ab und haben die oben beschriebenen Methoden.


Konkrete Subklassen von OutputStream brauchen nur die Form von write() ohne Argumente implementieren, um alle übrigen Methoden zum Arbeiten zu bringen (OutputStream hat in der Standardimplementierung die Methoden close() und flush(), die nichts bewirken). Subklassen von Writer müssen aber sowohl close() als auch die Form von write() mit den drei Argumenten implementieren.

ByteArrayOutputStream und CharArrayWriter

Das Gegenstück von ByteArrayInputStream (CharArrayReader), das einen Eingabestream für ein Byte- bzw. Zeichenarray erzeugt, ist ByteArrayOutputStream (CharArrayWriter ), der einen Ausgabestream an ein Byte- oder Zeichenarray übergibt:

OutputStream  s = new ByteArrayOutputStream();
Writer        w = new CharArrayWriter();
s.write(123);
w.write('\n');
. . .

Die Größe eines internen Byte- bzw. Zeichenarrays wächst nach Bedarf, um einen Datenstream jeder beliebigen Länge zu speichern. Sie können auf Wunsch eine Anfangskapazität als Hilfe für die Klasse festlegen:

OutputStream  s = new ByteArrayOutputStream(1024 * 1024);  // 1 Megabyte
Writer        w = new CharArrayWriter(1024 * 1024);


Damit haben Sie die ersten Beispiele des Erstellens von Ausgabestreams (und Writern) gesehen. Diese neuen Datenstreams werden an die einfachsten aller möglichen Datenquellen angehängt - ein Byte- bzw. Zeichenarray im Speicher des lokalen Rechners.

Nachdem ByteArrayOutputStream s (bzw. CharArrayWriter w) gefüllt wurde, kann er Daten an einen anderen Ausgabestream (oder Schreiber) ausgeben:

OutputStream           anotherOutputStream = getTheOtherOutputStream();
Writer                 anotherWriter       = getTheOtherWriter();
ByteArrayOutputStream  s = new ByteArrayOutputStream();
CharArrayWriter        w = new CharArrayWriter();

fillWithUsefulData(s);    fillWithUsefulData(w);
s.writeTo(anotherOutputStream);
w.writeTo(anotherWriter);

Außerdem kann er als Byte- oder Zeichenarray herausgezogen oder in eine Zeichenkette (string) konvertiert werden:

byte[]  bbuffer             = s.toByteArray();
char[]  cbuffer             = w.toCharArray();
String  streamString        = s.toString();
String  writerString        = w.toString();
String  streamUnicodeString = 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 Byte (normalerweise 0), um eine Unicode-Zeichenkette zu erzeugen.

ByteArrayOutputStream (und CharArrayWriter) haben zwei Utility-Methoden: Eine gibt die aktuelle Anzahl der im internen Byte- bzw. Zeichenarray gespeicherten Bytes (Zeichen) aus, die andere setzt das Array zurück, so daß der Datenstream von Anfang an erneut geschrieben werden kann:

int  sizeOfMyByteArray = s.size();
int  sizeOfMyCharArray = w.size();

s.reset();     // s.size() würde jetzt 0 zurückgeben
w.reset();     // w.size() würde jetzt 0 zurückgeben
s.write(123);
w.write('\n');
. . .


Writer hat einen Verwandten namens StringWriter, der ein wenig aus der Reihe tanzt - er hat kein Reader-Gegenstück und ist fast identisch mit CharArrayWriter (er fügt getBuffer() hinzu, was String zurückgibt, und implementiert writeTo(), toCharArray() und reset() nicht). Die Darstellung und Benutzung von Zeichenketten und Zeichenarrays sind sehr ähnlich, so daß diese Klasse überflüssig ist.

FileOutputStream und FileWriter

Eine der häufigsten Verwendungen von Datenstreams und historisch die älteste ist das Anhängen von Datenstreams an Dateien im Dateisystem. Hier wird beispielsweise ein solcher Ausgabestream (oder Writer) auf einem Unix-System erstellt:

OutputStream  s = new FileOutputStream("/Irgendein/Pfad/und/Dateiname");
Writer        w = new FileWriter("/Irgendein/Pfad/und/Dateiname.utf8");


Applets, die versuchen, solche Datenstreams im Dateisystem zu öffnen, zu lesen oder zu schreiben, können Sicherheitsverletzungen verursachen. Weitere Einzelheiten finden Sie im Hinweis unter FileInputStream und FileReader.


FileOutputStream (jedoch nicht FileWriter) hat auch einen Konstruktor, der einen String und einen booleschen Wert annimmt, um zu entscheiden, ob die Daten an die Datei angehängt werden müssen.

Sie können Datenstreams auch aus einem zuvor aktivierten FileDescriptor oder einer Datei erstellen:

OutputStream  s = new FileOutputStream(FileDescriptor.out);  /* Standardeingabe */
Writer        w = new FileWriter(FileDescriptor.err);        /* Standardfehler */
OutputStream  s = new FileOutputStream(new File("/Irgendein/Pfad/und/Dateiname"));
Writer        w = new FileWriter(new File("/Irgendein/Pfad/und/Dateiname.utf8"));

FileWriter ist eigentlich eine triviale Subklasse der weiteren writer-Klasse OutputStreamWriter , die jeden beliebigen OutputStream kapseln und ihn in einem Writer vom Typ char umwandeln kann. Somit besteht die Implementierung von FileWriter lediglich aus der Aufforderung von OutputStreamWriter, (selbst) einen FileOutputStream zu kapseln:

FileOutputStream  aFOS = new FileOutputStream("aFileName");

FileDescriptor  myFD = aFOS.getFD();

/* aFOS.finalize(); */  // Aktiviere close() bei automatischem Aufruf des Garbage-
                        //Collectors

Ein Aspekt ist ganz klar: getFD() gibt die Dateisignatur (FileDescriptor) zurück, auf der der Datenstream basiert. Der zweite Aspekt ist ein Kommentar, der Sie daran erinnern soll, daß Sie sich um das Schließen dieses Datenstreamtyps keine Gedanken machen müssen. Die Implementierung von finalize() besorgt das automatisch. (Siehe Erläuterungen unter FileInputStream und FileReader.)

FilterOutputStream und FilterWriter

Diese »abstrakten« Klassen (in Wirklichkeit ist nur FilterWriter abstrakt) bieten einen »Durchlauf« für alle Standardmethoden von OutputStream (oder Writer). Sie selbst enthalten einen anderen Datenstream weiter unten in der Filterkette, an die sie alle Methodenaufrufe abgeben. Sie implementieren nichts Neues, gestatten 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 eine Schreiboperation auf den gefilterten Datenstream s3 ausgeführt wird, wird die Anfrage s2 übergeben. Dann macht s2 genau das gleiche wie s1, und schließlich wird s aufgefordert, die Bytes bereitzustellen. Subklassen von FilterOutputStream führen eine gewisse Verarbeitung der durchfließenden Bytes aus. Diese im obigen Beispiel eher umständliche »Verkettung« kann eleganter geschrieben werden. Wie das gemacht wird, finden Sie unter der »Bruderklasse« FilterInputStream.

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

BufferedOutputStream und BufferedWriter

Das sind zwei der nützlichsten Datenstreams. Sie implementieren die vollen Fähigkeiten der Methoden von OutputStream und Writer, jedoch durch Verwendung eines gepufferten Byte- bzw. Zeichenarrays, der sich als Cache für weitere Schreiboperationen verhält. Dadurch werden die geschriebenen »Stückchen« von den größeren Blöcken, in denen Datenstreams am effizientesten geschrieben werden (z.B. in Peripheriegeräten, Dateien im Dateisystem oder im Netz), abgekoppelt.

BufferedOutputStream (BufferedWriter) ist eine der wenigen Klassen der Java-Bibliothek, die eine nichttriviale Version von flush() implementieren. Sie bewirkt, daß die geschriebenen Bytes (Zeichen) durch den Puffer geschoben und auf der anderen Seite ausgegeben werden. Da das Puffern von BufferedOutputStream (BufferedWriter ) so hilfreich ist, möchte man sich wünschen, daß jeder Ausgabestream (Writer) diese wertvollen Fähigkeiten irgendwie nutzt. Zum Glück können Sie jeden Ausgabestream (oder Writer) umgehen, um genau das zu erreichen:

OutputStream  s = new BufferedOutputStream(new FileOutputStream("foo"));
Writer        w = new BufferedWriter (new FileWriter("foo.utf8"));

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


BufferedWriter ist eigentlich keine Subklasse von FilterWriter, läßt sich aber wie eine solche »verschachteln«. Außerdem hat sie die einzigartige Methode newLine(), die Zeichen für neue Zeilenanfänge passend zu dem lokalen System setzt, auf dem Java läuft.

Jede von einem gefilterten Ausgabedatenstream (oder Writer) bereitgestellte Fähigkeit kann durch Verschachtelung von einem anderen Datenstream genutzt werden. Selbstverständlich ist durch Verschachtelung der Filterstreams jede Kombination dieser Fähigkeiten in jeder beliebigen Reihenfolge möglich.

DataOutputStream

Alle Methoden dieser Klasse sind in einer separaten Schnittstelle definiert, die von DataOutputStream und RandomAccessFile 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 Methoden höherer Ebene zum Lesen und Schreiben von Daten. Anstatt sich mit Bytes zu befassen, schreibt diese Schnittstelle die primitiven Typen von Java direkt:

void  write(int i)                                    throws IOException;
void  write(byte[]  buffer)                           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 spiegeln lediglich die drei Formen von write() wider, die Sie bereits kennengelernt haben. Die nächsten acht Methoden schreiben jeweils einen primitiven Typ. Die letzten drei Methoden schreiben eine aus Bytes oder Zeichen bestehende Zeichenkette in den Datenstream: die erste als 8-Bit-Bytes, die zweite als 16- Bit-Zeichen im binären Unicode und die dritte als speziellen Unicode-Datenstream (UTF-8) (der von readUTF() in DataInput gelesen werden kann).


Die Lesemethoden für vorzeichenlose Datentypen 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 von DataOutputStream implementierte Schnittstelle 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 Datenstream (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 Ein- und Ausgabeoperationen 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);

    . . .      // Verarbeite modifiedLine
    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();
        . . .      // Verarbeite b
        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 müssen Sie sich nicht um das Auffangen (catch) dieser Ausnahmen und deren angemessene Handhabung kümmern. Für die Praxis sollte Ihr Code weniger großzügig sein.

PrintStream und PrintReader

Ohne sich möglicherweise 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 Instanz von PrintStream, die sich in der Variablen out der Klasse System 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 Datenstreams an Standardausgabe, Standardfehler und Standardeingabe angehängt.

PrintStream ist ein Ausgabestream ohne brüderliches Gegenstück. PrintWriter ist einer von nur zwei Writern mit der gleichen Eigenschaft. Da sie normalerweise mit einer Bildschirmausgabe zusammenhängen, implementieren sie flush(). Ferner bieten sie die bekannten Methoden close() und write() sowie eine Fülle von Möglichkeiten zur Ausgabe der primitiven Typen und Zeichenketten von Java:

public void  write(int byteOrChar); // byte (PrintStream), char (PrintWriter)
public void  write(byte[]  buffer, int  offset, int  length); // PrintStream
public void  write(char[]  buffer, int  offset, int  length); // PrintWriter
public void  write(String  string);  // Die nächsten zwei Methoden nur in
                                     // PrintWriter
public void  write(String  string, int  offset, int  length); // PrintWriter
public void  flush();      // (Alles ab hier ist in beiden Klassen)
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();   // Leerzeile ausgeben

PrintStream (PrintWriter) kann auch benutzt werden, um einen Ausgabestream zu umwickeln wie eine Filterklasse (trotz der Tatsache, daß PrintWriter keine Subklasse von FilterWriter ist, läßt sie sich wie eine verschachteln):

PrintStream  s = new PrintStream(new FileOutputStream("foo"));
PrintWriter  w = new PrintWriter(new FileWriter("foo.utf8"));

s.println("Das ist die erste Textzeile der Datei foo.");
w.println("Das ist die erste Textzeile der Datei foo.utf8.");

Ein zweites Argument für den Konstruktor von PrintStream (oder PrintWriter) ist boolesch und bestimmt, ob der Datenstream automatisch »flushen« soll. Im Fall von true (wahr) wird nach jedem Zeichen, das eine neue Zeile setzt ('\n'), ein flush() gesendet. Bei der Form von write() mit drei Argumenten wird nach jeder Zeichengruppe ein flush() gesendet. PrintWriter handhabt das automatische Flush ein wenig anders - flush() wird nur nach dem Aufruf einer der Methoden println(...) gesetzt.

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 schreiben wir heute nur hier

public class  Cat {
    public static void  main(String argv[]) {
        DataInput  d = new DataInputStream(System.in);
        String     line;

     try {  while ((line = d.readLine()) != null)
            System.out.println(line);
        } catch (IOException  ignored) { }
    }
}

Damit wurden nun alle Subklassen von FilterOutputStream beschrieben. Wir wenden uns jetzt den direkten Subklassen von OutputStream zu.

ObjectOutputStream

Diese und die Bruderklasse ObjectInputStream unterstützen die Serialisation von Objekten (weitere Einzelheiten hierzu finden Sie unter ObjectInputStream). Alle Methoden, die Instanzen dieser Klasse verstehen, sind in der getrennten Schnittstelle ObjectOutput definiert, die ObjectOutputStream implementiert.

Die ObjectOutput-Schnittstelle

Diese Schnittstelle ist von der Schnittstelle DataOutput abgeleitet, wobei sie alle ihre Methoden erbt und darüber hinaus eine neue Methode der oberen Ebene bereitstellt, die einen komplexen Typenstream serialisierter Objektdaten unterstützt:

void  writeObject(Object  obj) throws IOException;

Im folgenden einfachen Beispiel wird ein Datenstream geschrieben, der im »Bruderbeispiel« (ObjectInputStream) in einem früheren Beispiel der heutigen Lektion gelesen wurde:

FileOutputStream    s   = new FileOutputStream("objectFileName");
ObjectOutputStream  oos = new ObjectOutputStream(s);

oos.writeInt(12345);               // Benutzt die DataOutput-Methode
oos.writeObject("Today");
oos.writeObject(new Date());
oos.flush();
s.close();

PipedOutputStream und PipedWriter

Diese und die Klassen PipedInputStream (PipedReader) bilden zusammen die Paare, die eine Unix-artige Pipe-Verbindung zwischen zwei Threads herstellen und sorgfältig die gesamte Synchronisation implementieren, die eine sichere Operation dieser Art von gemeinsamer Warteschlange ermöglicht. Die Verbindung wird so eingerichtet:

PipedInputStream   sIn  = new PipedInputStream();
PipedOutputStream  sOut = new PipedOutputStream(sIn);
PipedReader  wIn  = new PipedReader();
PipedWriter  wOut = new PipedWriter(wIn);

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


PipedOutputStream implementiert die zwei Formen von write() - eine ohne und eine mit drei Argumenten, während PipeWriter nur die Form mit drei Argumenten hat.

Zusammenhängende Klassen

Die übrigen Klassen und Schnittstellen in java.io ergänzen die Datenstreams, so daß ein komplettes Ein-/Ausgabesystem bereitgestellt wird. Drei davon werden im folgenden beschrieben.

Die Klasse File abstrahiert eine Datei 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 Zugriffsmodus (»r« oder »rw«) wird eine RandomAccessFile erzeugt. Sie umfaßt Implementierungen von DataInput und DataOutput in einer Klasse, jeweils auf »Zufallszugriff« auf eine Datei im Dateisystem abgestimmt. Zusätzlich zu diesen Schnittstellen bietet RandomAccessFile bestimmte herkömmliche Einrichtungen nach Unix-Art, z.B. seek() zum Suchen eines beliebigen Punkts in einer Datei.

Die Klasse StreamTokenizer greift einen Eingabestream (oder Reader) heraus und erzeugt daraus eine Folge von Token. Durch Überladen verschiedener darin enthaltener Methoden in Ihren Subklassen können Sie starke lexikale Parser erstellen.

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

Zusammenfassung

Heute haben Sie das allgemeine Konzept von Datenstreams gelernt und Beispiele mit Eingabestreams und Readern auf der Grundlage von Byte-Arrays, Dateien, Pipes, anderen Datenstreamfolgen und Stringpuffern sowie Eingabefiltern, Dateneingaben, Zeilennumerierung und Zurückschieben von Zeichen durchgearbeitet.

Sie haben auch die Gegenstücke dazu - die Ausgabestreams und Writer für Bytearrays, Dateien, Pipes und Ausgabefilter zum Schreiben typisierter Daten und Ausgabefilter - kennengelernt.

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

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

Java-Datenstreams bieten eine starke Grundlage, auf der Sie Multithreading-/Streaming-Schnittstellen der komplexesten Art entwickeln können, die in Browsern (z.B. Microsoft Internet Explorer oder Netscape Navigator) 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

Frage:
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?

Antwort:
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) {
. . . // Verarbeite Byte b
}

Das Problem bei dieser Kurzform entsteht, wenn read() den Wert 0xFF (0377) zurückgibt. Da dieser Wert ein Vorzeichen erhält, bevor der Test ausgeführt wird, erscheint er genauso wie der ganzzahlige Wert -1, der das Datenstreamende bezeichnet. Nur durch Speichern dieses Werts in einer getrennten ganzzahligen Variablen und späteres Umwandeln erreicht man das gewünschte Ergebnis. Ich habe die Umwandlung in byte aus Konsistenzgrü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 zurückgeben und für das Datenstreamende eine Ausnahme auswerfen).

Frage:
Wozu soll available() nützlich sein, wenn es manchmal die falsche Antwort ausgibt?

Antwort:
Erstens muß man zugeben, daß es bei vielen Datenstreams richtig reagiert. Zweitens kann seine Implementierung bei manchen Netzdatenstreams eine spezielle Anfrage senden, um bestimmte Informationen aufzudecken, die Sie andernfalls nicht einholen können (z.B. die Größe einer über 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 zurück bzw. andernfalls 0, was für Sie und Ihre Benutzer sichtbar wäre.

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

Antwort:
Eine übliche Verwendung dieses Schnittstellenpaars ist, wenn sich Objekte selbst zum Speichern oder Befördern über das Netz vorbereiten. Jedes Objekt implementiert Lese- und Schreibmethoden anhand dieser Schnittstellen, so daß sie sich selbst effektiv in einen Datenstream umwandeln, der später am anderen Ende als Kopie des Originalobjekts wiederhergestellt werden kann. Dieser Prozeß kann ab Version 1.1 über die neuen Ein- und Ausgabedatenstreams für Objekte automatisiert werden.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Ein Imprint des Markt&Technik Buch- und Software-Verlag GmbH.
Elektronische Fassung des Titels: Java 2 in 21 Tagen, ISBN: 3-8272-5578-3