BufferedReader
Kurz: Ein Character-Stream, der Lesezugriffe puffert und zeilenweises Lesen (readLine()) ermöglicht — wrappt meist einen FileReader oder InputStreamReader.
Genauer: Ohne Pufferung würde jeder einzelne Zeichenzugriff potenziell einen teuren Systemaufruf auslösen — BufferedReader liest größere Blöcke auf einmal in den Speicher und reicht sie von dort aus weiter, was besonders bei vielen kleinen Lesezugriffen deutlich schneller ist.
try (BufferedReader br = new BufferedReader(new FileReader("datei.txt"))) {
String zeile;
while ((zeile = br.readLine()) != null) {
System.out.println(zeile);
}
}Im Detail
BufferedReader implementiert das Decorator-Pattern: er “umhüllt” einen anderen Reader (meist FileReader) und fügt Pufferung hinzu, ohne dessen Code zu ändern. Die Standardpuffergröße ist 8192 Zeichen, lässt sich aber im Konstruktor anpassen:
try (BufferedReader br = new BufferedReader(new FileReader("log.txt"), 16384)) {
String zeile;
int zeilenzahl = 0;
while ((zeile = br.readLine()) != null) {
zeilenzahl++;
if (zeile.contains("ERROR")) {
System.out.println("Zeile " + zeilenzahl + ": " + zeile);
}
}
} catch (IOException e) {
System.err.println("Fehler beim Lesen: " + e.getMessage());
}readLine() gibt null zurück, sobald das Dateiende erreicht ist — das ist die übliche Abbruchbedingung für die Lese-Schleife (while ((zeile = br.readLine()) != null)), nicht etwa eine Exception. Für sehr große Dateien, bei denen nicht alle Zeilen gleichzeitig im Speicher gehalten werden sollen, ist dieses zeilenweise Lesen deutlich speicherschonender als z. B. Files.readAllLines(), das die komplette Datei auf einmal in eine List<String> lädt.
Warum Pufferung so viel ausmacht
Ohne BufferedReader würde ein FileReader bei jedem einzelnen read()-Aufruf potenziell einen Systemaufruf ans Betriebssystem auslösen, um ein einzelnes Zeichen von der Festplatte zu holen — bei einer Datei mit tausenden Zeichen wären das tausende teure Systemaufrufe. BufferedReader liest stattdessen einen ganzen Block (Standard: 8192 Zeichen) auf einmal in einen internen Speicherpuffer und bedient nachfolgende Lesezugriffe direkt aus diesem Puffer, bis er leer ist und ein neuer Block nachgeladen wird. Der Geschwindigkeitsunterschied kann bei großen Dateien den Faktor 10-100 ausmachen.
Character-Stream vs. Byte-Stream
BufferedReader gehört zur Familie der Character-Streams (Reader/Writer), die auf Textkodierung ausgelegt sind (z. B. UTF-8, wandelt Bytes automatisch in Zeichen um). Für rohe Binärdaten (Bilder, ZIP-Dateien) gibt es stattdessen Byte-Streams (InputStream/OutputStream), die keine Zeichenkodierung anwenden — siehe I/O Streams für die Übersicht.
Alternative: Streams-API und NIO
Seit Java 8 bietet Files.lines(Path.of("datei.txt")) eine funktionale Alternative, die einen Stream<String> zeilenweise liefert und sich gut mit filter()/map() kombinieren lässt:
try (var zeilen = Files.lines(Path.of("log.txt"))) {
long fehlerAnzahl = zeilen.filter(z -> z.contains("ERROR")).count();
System.out.println(fehlerAnzahl + " Fehlerzeilen gefunden");
} catch (IOException e) {
System.err.println("Fehler: " + e.getMessage());
}Intern nutzt auch Files.lines() Pufferung — der BufferedReader bleibt aber die klassische, überall verständliche Grundlage, auf der viele andere I/O-Muster in Java aufbauen.
Siehe auch: I/O Streams, BufferedWriter, try-with-resources