W3docs

Java Buffered Streams

Java I/O beschleunigen mit gepufferten Streams — BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream.

Die Kapitel zu Byte- und Zeichen-Streams haben die rohen APIs ehrlich beschrieben: Jeder Aufruf von FileInputStream.read() oder FileReader.read() ist ein Syscall. Ein Syscall kostet in der Größenordnung einer Mikrosekunde — für sich allein schnell, in einer engen Schleife katastrophal. Eine 1-MB-Datei Byte für Byte zu lesen entspricht einer Million Syscalls; dieselbe Datei mit einem 8-KB-Puffer sind 128. Der Unterschied in der Wanduhrzeit beträgt zwei bis drei Größenordnungen.

Die Buffered*-Dekoratoren sitzen zwischen Ihrem Code und dem rohen Stream. Sie halten ein byte[] (oder char[]) im Arbeitsspeicher und bedienen read()-Aufrufe daraus — zum Betriebssystem wird erst zugegangen, wenn der Puffer leer ist. Auf der Schreibseite sammeln sie kleine Schreibvorgänge in einem Puffer und übertragen sie erst ans Betriebssystem, wenn der Puffer voll ist oder Sie flush/close aufrufen. Dieselbe API, völlig andere Kosten.

Die vier gepufferten Klassen

KlasseUmhüllt
BufferedInputStreamEinen InputStream. Fügt einen internen byte[]-Puffer hinzu.
BufferedOutputStreamEinen OutputStream. Fügt einen internen byte[]-Puffer hinzu.
BufferedReaderEinen Reader. Fügt einen internen char[]-Puffer und die bekannte Methode readLine() hinzu.
BufferedWriterEinen Writer. Fügt einen internen char[]-Puffer und eine newLine()-Methode hinzu.

Alle vier umhüllen jeden Stream des passenden Typs — Datei, Socket, Pipe, In-Memory — nicht nur Datei-Streams:

BufferedInputStream  in  = new BufferedInputStream(new FileInputStream(path.toFile()));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(path.toFile()));
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));

Die Standard-Puffergröße beträgt 8192 Bytes/Zeichen — gewählt, um gängigen OS-Seitengrößen zu entsprechen. Sie können eine andere Größe an den zweiten Konstruktor übergeben, aber der Standard ist in nahezu allen Fällen ausreichend. Größere Puffer beschleunigen Abläufe nicht linear; sie verbrauchen nur mehr Speicher.

Die moderne API liefert Ihnen diese Dekoratoren bereits fertig zusammengesetzt:

BufferedReader r = Files.newBufferedReader(path);                            // UTF-8 by default
BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8);
InputStream    in  = new BufferedInputStream(Files.newInputStream(path));
OutputStream   out = new BufferedOutputStream(Files.newOutputStream(path));

Files.newBufferedReader / Files.newBufferedWriter umhüllen bereits die Bridge-Klasse mit dem richtigen Zeichensatz und einem BufferedReader/BufferedWriter. Für Text ist das der einzeilige Ersatz für den dreistufigen manuellen Stack.

BufferedReader.readLine()

Der Grund, warum BufferedReader die am häufigsten verwendete Klasse in java.io ist:

String readLine() throws IOException;          // a line, terminator stripped, or null at end
Stream<String> lines();                         // Java 8+: line stream

readLine erkennt \n, \r und \r\n als Zeilentrennzeichen und gibt die Zeile ohne den Trenner zurück. Es gibt null (nicht einen leeren String, nicht -1) am Streamende zurück — das standardmäßige Read-Line-Idiom:

try (BufferedReader r = Files.newBufferedReader(path)) {
  String line;
  while ((line = r.readLine()) != null) {
    process(line);
  }
}

r.lines() gibt einen Stream<String> für die funktionale Pipeline-Form zurück. Der Stream besitzt den offenen Reader, daher übernimmt das try-with-resources um den Reader das Schließen — lines() selbst benötigt kein eigenes Close.

Zwei wichtige Hinweise zu readLine(). Erstens alloziert es einen String pro Zeile. Für enge Log-Verarbeitungsschleifen, bei denen Allokierung eine Rolle spielt, ist das niedrigstufigere read(char[]) die bessere Wahl. Zweitens ist eine leere Zeile "" (ein leerer String), nicht null — die Datei endet erst, wenn readLine() null zurückgibt.

BufferedWriter.newLine()

Die entsprechende Komfortmethode auf der Schreibseite:

void newLine() throws IOException;             // platform line separator: \n on Unix, \r\n on Windows

newLine() schreibt das, was die JVM als Zeilentrennzeichen der aktuellen Plattform betrachtet. Das ist ein Feature, wenn Sie Dateien für menschliche Augen auf dem lokalen Rechner erzeugen; es ist ein Bug, wenn Sie Datendateien, Log-Dateien oder etwas erstellen, das für eine andere Maschine bestimmt ist. Das Internet läuft auf \n. Schreiben Sie \n immer explizit, wenn die Ausgabe portabel sein soll:

w.write("line one\n");                          // portable
w.newLine();                                    // platform-dependent: \n on Unix, \r\n on Windows

Dieselbe Empfehlung gilt für PrintWriter.println und den %n-Formatbezeichner — sie sind plattformabhängig. Verwenden Sie sie nur, wenn die Ausgabe für den lokalen Einsatz bestimmt ist.

Die Falle "Tail-Puffer nie geflusht"

Das ist der Bug, dem jede Java-Codebasis mindestens einmal begegnet:

// WRONG
BufferedWriter w = Files.newBufferedWriter(path);
w.write("hello");
return;                                          // 'hello' is sitting in the buffer; nothing on disk

Ein BufferedWriter schiebt Bytes erst dann ans Betriebssystem, wenn entweder der Puffer voll ist oder close() ausgeführt wird. Das Close weglassen und der Tail geht verloren — Files.size(path) ist 0 und Sie haben keine Ahnung warum. Die Lösung ist try-with-resources, ausnahmslos:

try (BufferedWriter w = Files.newBufferedWriter(path)) {
  w.write("hello");
}                                                // close() runs here; tail is flushed

Wenn Sie die Daten vor dem Close auf der Festplatte benötigen — ein Log-Tail-Watcher oder ein anderer Prozess, der die Datei abfragt — rufen Sie flush() explizit auf. Der Puffer wird nicht nach jedem Schreibvorgang automatisch geleert; das ist der Preis für einen Puffer überhaupt.

Mark und Reset

BufferedReader und BufferedInputStream unterstützen beide eine kleine "Vorausschauen und Zurückspulen"-API:

in.mark(1024);                                   // remember this position; allow up to 1024 bytes of lookahead
int b = in.read();
in.reset();                                      // back to the marked position

Das ist die einzige java.io-API, mit der Sie ein Byte/Zeichen lesen und es anschließend zurücklegen können. Sie bildet die Grundlage für Code, der "die ersten paar Bytes liest, um das Format zu ermitteln" — UTF-8-BOM-Erkennung, Magic-Number-Sniffing, Parser-Übergabe. Ohne Pufferung ist das nicht möglich: Die rohen Streams haben die Bytes nicht mehr, sobald sie gelesen wurden.

Wenn Pufferung nicht hilft

Zwei Fälle, in denen das Hinzufügen eines Buffered*-Dekorators nichts bringt:

  • Die Quelle befindet sich bereits im Arbeitsspeicher. ByteArrayInputStream und StringReader bedienen read() bereits aus einem byte[]/String im Speicher; es gibt keine Syscalls zu amortisieren.
  • Sie verwenden Files.readString, Files.readAllBytes, Files.write oder transferTo. Diese Aufrufe führen ihre eigene blockweise I/O mit einem großen internen Puffer durch. Diese in BufferedInputStream einzuwickeln ist redundant — das JDK hat bereits gepuffert.

Der Fall, bei dem Pufferung hilft, ist der ursprüngliche: Sie lesen oder schreiben kleine Blöcke (ein einzelnes Byte, eine einzelne Zeile, einen printf-Aufruf) und die Quelle/das Ziel ist eine echte Datei, ein Socket oder eine Pipe.

Ein Praxisbeispiel: dieselbe Last, mit und ohne

Das folgende Programm kopiert denselben 32-KB-Blob Byte für Byte von einer temporären Datei in eine andere — einmal mit rohem FileInputStream/FileOutputStream, einmal mit BufferedInputStream/BufferedOutputStream, einmal mit transferTo zum Vergleich. Die ausgegebenen Wanduhrzeiten machen die Kosten des fehlenden Puffers sichtbar. Anschließend liest das Beispiel die Zeilen der Datei über einen BufferedReader und demonstriert die "Flush vergessen"-Falle auf der Schreibseite.

java— editable, runs on the server

Was aus dem Durchlauf mitgenommen werden sollte:

  • Die rohe Byte-für-Byte-Kopie war um Größenordnungen langsamer als die gepufferte. Der Schleifenkörper war identisch; die einzige Änderung war das Einwickeln der Datei-Streams in BufferedInputStream/BufferedOutputStream. Das ist der einzige Grund, warum diese Dekoratoren existieren — dieselbe API, drastisch weniger Syscalls.
  • transferTo war genauso schnell wie die gepufferte Version (oder schneller). Für "Bytes von A nach B kopieren ohne Transformation" ist transferTo die richtige Wahl — es puffert intern, und das JDK hat die Schleife optimiert. Greifen Sie darauf zurück, bevor Sie Ihre eigene schreiben.
  • Files.newBufferedReader gab direkt einen BufferedReader zurück. Beachten Sie, dass wir nie new BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8)) geschrieben haben — diesen dreistufigen Stack verbirgt die Factory. readLine() kam aus diesem Stack ohne weiteren Aufwand.
  • Der undichte Writer zeigte 0 bytes vor flush(). Diese Zeichen befanden sich im In-Memory-Puffer, nicht auf der Festplatte. Das Aufrufen von flush() schob sie heraus; ohne das explizite Flush (oder ein ordnungsgemäßes close()) wären sie verloren gegangen. Deshalb ist try-with-resources um gepufferte Writer nicht optional — es ist der Vertrag, der den Schreibvorgang sichtbar macht.
  • Die BufferedReader.readLine()-Schleife ist die gebräuchlichste Form der Textverarbeitung in Java. Prägen Sie sich die Form while ((line = r.readLine()) != null) ein: Die Zuweisung in der Bedingung ist hier idiomatisch, und das null-Sentinel (nicht ein leerer String) ist die Endbedingung der Schleife.

Wie es weitergeht

Pufferung löst die Kosten eines Syscalls pro Aufruf, ändert aber nicht, was die Bytes bedeuten. Das nächste Kapitel, Java DataInput and DataOutput Streams, behandelt die Dekoratoren, die Java-Primitive in einem portablen Binärformat lesen und schreiben — die Schicht, mit der Sie einen int in eine Datei schreiben und ihn als int auf einem anderen Betriebssystem zurücklesen können.

Übungen

Übung
Was passiert mit den Daten, die durch `w.write('hello')` geschrieben wurden, wenn Sie vergessen, einen `BufferedWriter` zu schließen (und `flush()` nie aufrufen)?
Was passiert mit den Daten, die durch `w.write('hello')` geschrieben wurden, wenn Sie vergessen, einen `BufferedWriter` zu schließen (und `flush()` nie aufrufen)?
Was this page helpful?