Streams

Klassenhierarchie: Input Streams

Man erkennt an der Klassenhierarchie gut den Aufbau. Ausgehend von der Basisklasse InputStream, die alle grundlegenden Operationen auf den lesenden Streams festlegt, gibt es für die unterschiedlichen Datenquellen spezielle Subklassen. Diese sind:

  • FileInputStream – Dateien
  • ZipInputStream – Zip-Dateien
  • SocketInputStream – Netzwerk-Sockets
  • ObjectInputStream – Serialisierung
  • PipedInputStream – Verbindung von Threads
  • ByteArrayInputStream – Lesen aus einem Byte-Array
  • FilterInputStream – Basisklasse für Filter
  • BufferedInputStream – Pufferung
  • DataInputStream – Lesen von Daten

Die Methoden von InputStream

  • abstract int read() – liest ein einzelnes Byte vom Stream
  • int read(byte[] b) – liest vom Stream in das Byte-Array
  • int read(byte[] b, int off, int len) – liest maximal len Bytes in das Byte-Array ab position off
  • long skip(long n) – überspringt n Bytes
  • int available() – Anzahl der Bytes die voraussichtlich gelesen werden können ohne das die Leseoperation blockiert
  • void close() – schließt den Stream
  • void mark(int readlimit) – markiert die aktuelle Position im Stream und vergisst sie erst nach readlimit Bytes die daraufhin gelesen wurden
  • void reset() – springt zu der mit mark() markierten Stelle im Stream zurück
  • boolean markSupported() – zeigt an, ob der Stream überhaupt das setzen einer Marke mit mark() unterstützt

Bezüglich der genauen Arbeitsweise der Methoden und ihrer Dokumentation sei auf die JavaDoc der Klassen verwiesen.

Bemerkenswert ist aber die Tatsache, dass die Methode read() ein int als Rückgabetyp hat und kein byte, obwohl ein InputStream die Daten byteweise anliefert. Der größere Datentyp ist nötig, da durch eine -1 signalisiert wird, dass der Stream zu Ende ist. Da aber -1 ein gültiger Byte-Wert ist, hat man hier den größeren Datentyp nehmen müssen. Aus diesem Grund ist es falsch den Rückgabewert von read() vor dem Vergleich mit -1 zu casten. Erst muss mit -1 (als int) verglichen werden, dann gecastet, sonst bricht der Lesevorgang möglicherweise zu früh ab, nämlich wenn in den Daten zufällig ein 0xFF vorkommt, das vorzeichenbehaftet einer -1 entspricht.

Bytes von einem Stream lesen

InputStream fis = new FileInputStream("/tmp/test.txt");

int daten;

while ((daten = fis.read()) != -1) {
    byte b = (byte) daten;
    // jetzt kann man etwas sinnvolles mit den Bytes machen, die aus
    // der Datei gelesen wurden
}

fis.close();

In diesem Beispiel wird die -1 richtig behandelt. Erst nach dem Vergleich erfolgt der cast.

InputStream fis = new FileInputStream("/tmp/test.txt");

byte b;

while ((b = (byte) fis.read()) != -1) { // FEHLER!!
    // jetzt kann man etwas sinnvolles mit den Bytes machen, die aus
    // der Datei gelesen wurden
}

fis.close();

In diesem Beispiel wird die -1 falsch behandelt. Es wird erst gecastet und dann verglichen. Der Lesevorgang wird potenziell zu früh beendet, weil -1 ein möglicher Byte-Wert ist und so der Lesevorgang abbricht, wenn dieser Wert in der Quelle auftritt.

Blöcke von einem Stream lesen

InputStream fis = new FileInputStream("/tmp/test.txt");

byte[] daten = new byte[1024];
int bytesRead;

while ((bytesRead = fis.read(daten)) > -1) {
    // jetzt kann man etwas sinnvolles mit den Bytes machen, die aus
    // der Datei gelesen wurden
}

fis.close();

Das Lesen einzelner Bytes ist ineffizient, daher bietet InputStream eine Methode an, mit der man die Bytes blockweise in ein Byte-Array lesen kann. Hier signalisiert der Rückgabewert der Methode, wie viele Bytes gelesen wurden. -1 zeigt das Ende des Streams an. 0 ist ein korrekter Rückgabewert, da es beim Lesen vom Netzwerk vorkommen kann, dass noch keine neuen Daten eingetroffen sind, die Verbindung aber noch steht. Hier muss man einfach erneut read aufrufen.

Klassenhierarchie: Output Streams

Auch bei den OutputStreams gibt es eine Basisklasse, die das Verhalten festlegt und eine Reihe von konkreten Klassen für die verschiedenen Senken. Diese sind:

  • FileOutputStream – Dateien
  • SocketOutputStream – Netzwerk-Sockets
  • ObjectOutputStream – Serialisierung
  • PipedOutputStream – Verbindung von Threads
  • FilterOutputStream – Basisklasse für Filter
  • ByteArrayOutputStream – Schreiben in ein Byte-Array
  • BufferedOutputStream – Pufferung
  • DataOutputStream – Schreiben von Daten

Die Methoden von OutputStream

  • abstract void write(int b) – schreibt ein Byte
  • void write(byte[] b) – schreibt das gesamte Byte-Array
  • void write(byte[] b, int off, int len) – schreibt len Bytes aus dem Byte-Array beginnend bei off
  • void flush() – leert interne Puffer und bringt die Daten auf die Platte
  • void close() – schließt den Stream (impliziert ein flush())

Auch hier sei auf die JavaDoc der Klassen verwiesen. Die Tatsache, dass die write-Methode einen int als Parameter nimmt, hat keine besondere Bedeutung, sondern dient nur dazu, dass sie symmetrisch zur read-Methode ist. Hierdurch kann man den Wert, der von read gelesen wurde direkt write übergeben ohne ihn konvertieren zu müssen.

Bytes auf einen Stream schreiben

OutputStream fos = new FileOutputStream("/tmp/myfile");

fos.write(0xca);
fos.write(0xfe);
fos.write(0xba);
fos.write(0xbe);

fos.close();

Blöcke auf einen Stream schreiben

OutputStream fos = new FileOutputStream("/tmp/myfile");

byte[] daten = { (byte) 0xca, (byte) 0xfe,
        (byte) 0xba, (byte) 0xbe };

fos.write(daten);
fos.write(daten, 0, 2);
fos.write(daten, 0, 2);
fos.write(daten, 2, 2);
fos.write(daten, 2, 2);
fos.close();

FileInputStream

  • FileInputStream
    • public FileInputStream(String name)
      throws FileNotFoundException
    • public FileInputStream(File file)
      throws FileNotFoundException

FileInputStream erbt alle Methoden von InputStream und fügt nur zwei Konstruktoren hinzu, um eine Datei zum Öffnen auswählen zu können. Man kann die Datei entweder über den Dateinamen (als String) oder ein spezielles File-Objekt öffnen (das noch später erläutert wird).

FileOutputStream

  • FileOutputStream
    • public FileOutputStream(String name)
      throws FileNotFoundException
    • public FileOutputStream(String name, boolean append)
      throws FileNotFoundException
    • public FileOutputStream(File file)
      throws FileNotFoundException
    • public FileOutputStream(File file, boolean append)
      throws FileNotFoundException

FileOutputStream verhält sich analog zum FileInputStream nur dass hier über einen weiteren Konstruktorparameter append angegeben werden kann, ob die Datei überschrieben (false) werden soll oder die geschriebenen Daten hinten an die Datei angehängt werden sollen (true).

Beispiel: Anhängen an Stream

byte[] daten = { (byte) 0xca, (byte) 0xfe,
        (byte) 0xba, (byte) 0xbe };

OutputStream fos = new FileOutputStream("/tmp/myfile");
fos.write(daten);
fos.close();

OutputStream fos2 = new FileOutputStream("/tmp/myfile", true);
fos2.write(daten);
fos2.close();

Beispiel: Einfaches Kopierprogramm

// Quell und Zieldatei
String quelle = "/tmp/quelle.txt";
String ziel = "/tmp/ziel.txt";

// Streams für Quell und Zieldatei
InputStream in = new FileInputStream(quelle);
OutputStream out = new FileOutputStream(ziel);
byte[] buffer = new byte[1024];  // Puffer für Dateiinhalt
int gelesen;                     // Anzahl der gelesenen Bytes

// Daten aus Quell- in Zieldatei kopieren
while ((gelesen = in.read(buffer)) > -1) {
    out.write(buffer, 0, gelesen);
}

// Streams schließen
in.close();
out.close();

Beispiel: Naive Prüfsummenbestimmung

InputStream in = new FileInputStream("/tmp/quelle.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();

int daten;
while ((daten = in.read()) > -1) {
    out.write(daten);
}

in.close();
out.close();

byte[] bytes = out.toByteArray();
long sum = 0;

for (int i = 0; i < bytes.length; i++) {
    sum += bytes[i];
}
System.out.println(sum);

Filter Streams und Decorator Pattern

  • Zusätzlich zu den Streams, die direkt mit einer Quelle oder Senke (Datei, Socket etc.) verbunden sind, gibt es die Filter-Streams
  • Filter-Streams können hinter einen anderen Stream geschaltet werden (Decorator Pattern) und können die Daten entsprechend verändern (daher Filter-Streams)

Wenn man mehrere Streams miteinander verkettet, fließen die Daten von der Quelle (oder zur Senke) nacheinander durch die Streams. Diese können die Daten dann entsprechend verändern oder umwandeln.

Beispiel: Buffering

String quelle = "/tmp/quelle.txt";
String ziel = "/tmp/ziel.txt";

InputStream in = new BufferedInputStream(
        new FileInputStream(quelle));

OutputStream out = new BufferedOutputStream(
        new FileOutputStream(ziel));

byte[] buffer = new byte[1024];
int gelesen;

while ((gelesen = in.read(buffer)) > -1) {
    out.write(buffer, 0, gelesen);
}

in.close();
out.close();

Ein häufig eingesetzter Filter-Stream ist der BufferedInputStream bzw. der BufferedOutputStream. Diese puffern (buffer) die Daten und erlauben so eine schnellere Verarbeitung, weil nicht für jede Operation bis auf die Quelle/Senke durchgegriffen werden muss.

DataOutputStream / DataInputStream

  • DataOutputStream schreibt primitive Datentypen und String in einen anderen Stream und erlaubt so den plattformunabhängigen Datenaustausch
  • DateInputStream liest die von DataOutputStream geschriebenen Daten wieder ein

Man kann zwar mit den normalen Streams problemlos byte-weise beliebige Daten schreiben und lesen, aber die Umwandlung der längeren Datentypen (short, char, int, long, float, double) obliegt dann den Programmierer:innen. Dies ist insofern problematisch, als z. B. die Reihenfolge der Bytes eines Long nicht plattformübergreifend festgelegt ist – Stichwort little und big endian.

Die Mühe der Umwandlung kann man sich sparen, wenn man DataOutputStream und DataInputStream verwendet. Hier sind für alle primitiven Datentypen und Strings entsprechende Methoden vorgesehen, um sie in einem einheitlichen Format zu lesen und zu schreiben.

Methoden von DataOutputStream

  • void writeBoolean(boolean v) throws IOException
  • void writeByte(int v) throws IOException
  • void writeShort(int v) throws IOException
  • void writeChar(int v) throws IOException
  • void writeInt(int v) throws IOException
  • void writeLong(long v) throws IOException
  • void writeFloat(float v) throws IOException
  • void writeDouble(double v) throws IOException
  • void writeBytes(String s) throws IOException
  • void writeChars(String s) throws IOException
  • void writeUTF(String s) throws IOException

Beispiel: DataOutputStream

DataOutputStream out = new DataOutputStream(
        new BufferedOutputStream(
                new FileOutputStream("/tmp/daten")));

out.writeUTF("** Datendatei **");
out.writeUTF("Datum");
out.writeLong(new Date().getTime());
out.writeUTF("PI");
out.writeDouble(Math.PI);

out.close();

Methoden von DataInputStream

  • 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 readUTF() throws IOException

Beispiel: DataInputStream

DataInputStream dis = new DataInputStream(
        new BufferedInputStream(
                new FileInputStream("/tmp/daten")));

String header = dis.readUTF();
String datumTag = dis.readUTF();
Date datum = new Date(dis.readLong());
String PITag = dis.readUTF();
double pi = dis.readDouble();
dis.close();

System.out.println(header);
System.out.println(datumTag + " " + datum);
System.out.println(PITag + " " + pi);
** Datendatei **
Datum Sun Sep 12 22:49:27 CEST 2010
PI 3.141592653589793

Copyright © 2025 Thomas Smits