Die Klassen OutputStream und InputStream

Die abstrakte Basisklasse OutputStream

Wenn wir uns den OutputStream anschauen, dann sehen wir auf den ersten Blick, dass hier alle wesentlichen Operationen rund um das Schreiben versammelt sind. Der Clou bei allen Datenströmen ist, dass spezielle Unterklassen wissen, wie sie genau die vorgeschriebene Funktionalität implementieren. Das heißt, dass ein konkreter Datenstrom, der in Dateien oder in eine Netzwerkverbindung schreibt, weiß, wie Bytes in Dateien oder ins Netzwerk kommen. Und an der Stelle ist Java mit seiner Plattformunabhängigkeit am Ende, denn auf einer so tiefen Ebene können nur native Methoden die Bytes schreiben.

abstract class java.io.OutputStream
implements Closeable, Flushable

  • abstractvoidwrite(intb)throwsIOException
    Schreibt ein einzelnes Byte in den Datenstrom.
  • voidwrite(byte[]b)throwsIOException
    Schreibt die Bytes aus dem Array in den Strom.
  • voidwrite(byte[]b,intoff,intlen)throwsIOException
    Schreibt Teile des Byte-Feldes, nämlich len Byte ab der Position off, in den Ausgabestrom.
  • voidclose()throwsIOException
    Schließt den Datenstrom. Einzige Methode aus Closeable.
  • voidflush()throwsIOException
    Schreibt noch im Puffer gehaltene Daten. Einzige Methode aus der Schnittstelle Flushable.

Die IOException ist keine RuntimeException und muss behandelt werden.

Beispiel: Die Klasse ByteArrayOutputStream ist eine Unterklasse von OutputStream und speichert intern alle Daten in einem byte-Array. Schreiben wir ein paar Daten mit den drei gegeben Methoden hinein:

byte[] bytes = { 'O', 'N', 'A', 'L', 'D' };

//                0    1    2    3    4

ByteArrayOutputStream out = new ByteArrayOutputStream();

try {

  out.write( 'D' );          // schreibe D

  out.write( bytes );        // schreibe ONALD

  out.write( bytes, 1, 2 );  // schreibe NA

  System.out.println( out.toString( StandardCharsets.ISO_8859_1.name() )  );

}

catch ( IOException e ) {

  e.printStackTrace();

}

Über konkrete und abstrakte Methoden *

Zwei Eigenschaften lassen sich an den Methoden vom OutputStream ablesen: zum einen, dass nur Bytes geschrieben werden, und zum anderen, dass nicht wirklich alle Methoden abstract sind und von Unterklassen für konkrete Ausgabeströme überschrieben werden müssen. Nur write(int) ist abstrakt und elementar. Das ist trickreich, denn tatsächlich lassen sich die Methoden, die ein Byte-Array schreiben, auf die Methode abbilden, die ein einzelnes Byte schreibt. Wir werfen einen Blick in den Quellcode der Bibliothek:

java/lang/OutputStream,java, Ausschnitt

public abstract void write(int b) throws IOException;
 
 public void write(byte[] b) throws IOException {
   write(b, 0, b.length);
 }
 
 public void write(byte b[], int off, int len) throws IOException {
   if (b == null) {
     throw new NullPointerException();
   } else if ((off < 0) || (off > b.length) || (len < 0) ||
              ((off + len) > b.length) || ((off + len) < 0)) {
     throw new IndexOutOfBoundsException();
   } else if (len == 0) {
     return;
   }
   for (int i = 0 ; i < len ; i++) {
     write(b[off + i]);
   }
 }

An beiden Implementierungen ist zu erkennen, dass sie die Arbeit sehr bequem an andere Methoden verschieben. Doch diese Implementierung ist nicht optimal! Stellen wir uns vor, ein Dateiausgabestrom überschreibt nur die eine abstrakte Methode, die nötig ist. Und nehmen wir weiterhin an, dass unser Programm nun immer ganze Byte-Felder schreibt, etwa eine 5-MiB-Datei, die im Speicher steht. Dann werden für jedes Byte im Byte-Array in einer Schleife alle Bytes der Reihe nach an eine vermutlich native Methode übergeben. Wenn es so implementiert wäre, könnten wir die Geschwindigkeit des Mediums überhaupt nicht nutzen, zumal jedes Dateisystem Funktionen bereitstellt, mit denen sich ganze Blöcke übertragen lassen. Glück-licherweise sieht die Implementierung nicht so aus, da wir in dem Modell vergessen haben, dass die Unterklasse zwar die abstrakte Methode implementieren muss, aber immer noch andere Methoden überschreiben kann. Ein Blick auf die Unterklasse FileOutputStream bestätigt dies.

Hinweis: Ruft eine Oberklasse eine abstrakte Methode auf, die spätger die Unterklasse implementiert, ist das ein Entwurfsmuster mit dem Namen Schablonenmuster oder auf Englisch template pattern.

Gleichzeitig stellt sich die Frage, wie ein OutputStream, der die Eigenschaften für alle erdenklichen Ausgabeströme vorschreibt, wissen kann, wie ein spezieller Ausgabestrom etwa mit close() geschlossen wird oder seine gepufferten Bytes mit flush() schreibt – die Methoden müssten doch auch abstrakt sein! Das weiß OutputStream natürlich nicht, aber die Entwickler haben sich dazu entschlossen, eine leere Implementierung anzugeben. Der Vorteil besteht darin, dass Unterklassen nicht verpflichtet werden, immer die Methoden zu überschreiben, auch wenn sie sie gar nicht nutzen wollen.

Ein Datenschlucker *

Damit wir sehen können, wie alle Unterklassen prinzipiell mit OutputStream umgehen, wollen wir eine Klasse entwerfen, die alle ihre gesendeten Daten verwirft. Die Klasse ist mit dem Unix-Device /dev/null vergleichbar. Die Implementierung muss lediglich die abstrakte Methode write(int) überschreiben.

 public final class NullOutputStream extends java.io.OutputStream {

  @Override public void write( int b ) { /* Empty */ }
 }

Da close() und flush() ohnehin schon mit einem leeren Block implementiert sind, brauchen wir sie nicht noch einmal zu überschreiben.

Die abstrakte Basisklasse InputStream

Das Gegenstück zu OutputStream ist InputStream; jeder binäre Eingabestrom wird durch die abstrakte Klasse InputStream repräsentiert. Die Konsoleneingabe System.in ist vom Typ InputStream. Die Klasse bietet mehrere readXXX(…)-Methoden und ist auch ein wenig komplexer als OutputStream.

abstract class java.io.InputStream
implements Closeable

  • intavailable()throwsIOException
    Gibt die Anzahl der verfügbaren Zeichen im Datenstrom zurück, die sofort ohne Blockierung gelesen werden können.
  • abstractintread()throwsIOException
    Liest ein Byte aus dem Datenstrom und liefert ihn zurück. Die Rückgabe ist -1, wenn der Datenstrom keine Daten mehr liefern kann. Der Rückgabetyp ist int, weil -1 (0xFFFFFFFF) das Ende des Datenstroms anzeigt, und ein -1 als byte (das wäre 0xFF) nicht von einem normalen Datum unterschieden werden könnte.
  • intread(byte[]b)throwsIOException
    Liest bis zu length Bytes aus dem Datenstrom und setzt sie in das Array b. Die tatsächliche Länge der gelesenen Bytes wird zurückgegeben und muss nicht b.length sein, es können auch weniger Bytes gelesen werden. In der OutputStream einfach als return read(b, 0, b.length); implementiert.
  • intread(byte[]b,intoff,intlen)throwsIOException
    Liest den Datenstrom aus und setzt die Daten in das Byte-Array b, an der Stelle off Zudem begrenzt len die maximale Anzahl der zu lesenden Bytes. Intern ruft die Methode zunächst read() auf und wenn es zu einer Ausnahme kommt, endet auch damit unsere Methode mit einer Ausnahme. Es folgenen wiederholten Aufrufe von read(), die dann enden, wenn read() die Rückgabe -1 liefert. Falls es zu einer Ausnahme kommt, wird diese aufgefangen und nicht gemeldet.
  • intreadNBytes(byte[]b,intoff,intlen)throwsIOException
    Versucht len viele Bytes aus dem Datenstrom zu lesen und in das Byte-Array zu setzen. Im Gegensatz zu read(byte[], int, int) übernimmt readNBytes(…) mehrere Anläufe len viele Daten zu beziehen. Dabei greift es auf read(byte[], int, int) zurück. Neue Methode in Java 9.
  • byte[] readAllBytes() throws IOException
    Liest alle verbleibenden Daten aus den Datenstrom und liefert ein Array mit diesen Bytes aus Rückgabe. Neue Methode in Java 9.
  • long transferTo(OutputStream out) throws IOException
    Liest alle Bytes aus dem Datenstrom aus und schreibt sie in out. Wenn es zu einer Ausnahme kommt, wird empfohlen, die Datenströme beide zu schließen. Neue Methode in Java 9.
  • longskip(longn)throwsIOException
    Überspringt eine Anzahl von Zeichen. Die Rückgabe gibt die tatsächlich gesprungenen Bytes zurück, was nicht mit n identisch sein muss.
  • booleanmarkSupported()
    Gibt einen Wahrheitswert zurück, der besagt, ob der Datenstrom das Merken und Zurücksetzen von Positionen gestattet. Diese Markierung ist ein Zeiger, der auf bestimmte Stellen in der Eingabedatei zeigen kann.
  • voidmark(intreadlimit)
    Merkt sich eine Position im Datenstrom.
  • voidreset()throwsIOException
    Springt wieder zu der Position zurück, die mit mark() gesetzt wurde.
  • voidclose()throwsIOException
    Schließt den Datenstrom. Operation aus der Schnittstelle Closeable.

Auffällig ist, dass bis auf mark(int) und markSupported() alle Methoden im Fehlerfall eine IOException auslösen.

Hinweis: available() liefert die Anzahl der Bytes, die ohne Blockierung gelesen werden können. („Blockieren“ bedeutet, dass die Methode nicht sofort zurückkehrt, sondern erst wartet, bis neue Daten vorhanden sind.) Die Rückgabe von available() sagt nichts darüber aus, wie viele Zeichen der InputStream insgesamt hergibt. Während aber bei FileInputStream die Methode available() üblicherweise doch die Dateilänge liefert, ist dies bei den Netzwerk-Streams im Allgemeinen nicht der Fall.

Über Christian Ullenboom

Ich bin Christian Ullenboom und Autor der Bücher ›Java ist auch eine Insel. Einführung, Ausbildung, Praxis‹ und ›Java SE 8 Standard-Bibliothek. Das Handbuch für Java-Entwickler‹. Seit 1997 berate ich Unternehmen im Einsatz von Java. Sun ernannte mich 2005 zum ›Java-Champion‹.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.