EMZETT.
Login

I/O Streams

Kurz: Das Grundprinzip von Javas Ein-/Ausgabe-API: Daten fließen als kontinuierlicher Strom (Stream) zwischen einer Quelle (Datei, Netzwerk, Konsole) und dem Programm.

Genauer: Java unterscheidet Byte-Streams (InputStream/OutputStream, für Binärdaten) und Character-Streams (Reader/Writer, für Textdaten mit Zeichenkodierung). Streams lassen sich verketten (“Wrapping”): ein BufferedReader wrappt z. B. einen FileReader, um Pufferung hinzuzufügen, ohne die Basisklasse selbst zu ändern.

Im Detail

Byte-Streams (Binärdaten)          Character-Streams (Text)
InputStream  ──wraps──▶  BufferedInputStream    Reader ──wraps──▶ BufferedReader
    │                                              │
FileInputStream                              FileReader / InputStreamReader

Das “Wrapping”-Prinzip (Decorator-Pattern) ist der zentrale Baustein der gesamten Stream-API: eine Basisklasse übernimmt die eigentliche Quelle (Datei, Netzwerk-Socket, Konsole), und zusätzliche Wrapper-Klassen fügen Funktionalität hinzu, ohne die Basisklasse selbst zu ändern:

// Character-Stream: Datei -> Zeichenkodierung -> Pufferung
try (BufferedReader br = new BufferedReader(
        new InputStreamReader(new FileInputStream("text.txt"), StandardCharsets.UTF_8))) {
    String zeile = br.readLine();
}

InputStreamReader ist selbst schon ein Wrapper, der einen rohen Byte-Stream (InputStream) in einen Character-Stream (Reader) übersetzt, indem er die angegebene Zeichenkodierung anwendet — genau an dieser Stelle entscheidet sich, ob z. B. Umlaute korrekt gelesen werden oder als kaputte Zeichen erscheinen. System.in/System.out sind selbst schon fertige Byte-Streams für Konsoleneingabe/-ausgabe, weshalb Scanner (für User Input) intern ebenfalls auf dieses Wrapping-Prinzip zurückgreift.

Warum überhaupt Streams statt “die ganze Datei auf einmal”?

Der Kern des Stream-Konzepts: Daten müssen nicht komplett im Speicher liegen, bevor die Verarbeitung beginnt. Eine 10-GB-Logdatei lässt sich zeilenweise durchgehen, ohne je mehr als eine Zeile gleichzeitig im Speicher zu halten — bei Files.readAllBytes()/Files.readString() (praktisch für kleine Dateien) müsste dagegen die komplette Datei auf einmal in den Speicher passen. Dieser Kompromiss zwischen Bequemlichkeit (alles auf einmal, eine Zeile Code) und Speichereffizienz (Stream-basiert, mehr Code) zieht sich durch die gesamte I/O-API.

NIO.2 als moderne Alternative

Seit Java 7 bietet das java.nio.file-Paket (kurz “NIO.2”, um es vom älteren java.nio zu unterscheiden) mit Files und Path eine deutlich kompaktere API für viele Standardfälle, ohne die klassischen Stream-Klassen zu ersetzen:

// Klassisch mit Streams
try (BufferedReader br = new BufferedReader(new FileReader("klein.txt"))) {
    String inhalt = br.lines().collect(Collectors.joining("\n"));
}
 
// Äquivalent mit NIO.2 (nur für Dateien, die komplett in den Speicher passen)
String inhalt = Files.readString(Path.of("klein.txt"));

Intern greift auch NIO.2 für große Dateien wieder auf dieselben Stream-Klassen zurück (Files.newBufferedReader() liefert z. B. einen ganz normalen BufferedReader) — die beiden APIs ergänzen sich, statt sich gegenseitig zu ersetzen.

System.in, System.out, System.err als vordefinierte Streams

Drei Standard-Streams stehen in jedem Java-Programm ohne eigenes Öffnen zur Verfügung, verwaltet vom Betriebssystem/der JVM:

System.out.println("Normale Ausgabe"); // Standardausgabe (stdout)
System.err.println("Fehlerausgabe");   // Standardfehlerausgabe (stderr) - separater Kanal
Scanner scanner = new Scanner(System.in); // Standardeingabe (stdin)

Die Trennung zwischen System.out und System.err erlaubt es, beide beim Ausführen unabhängig voneinander umzuleiten (z. B. java Programm > log.txt 2> fehler.txt) — normale Ausgaben landen in einer Datei, Fehlermeldungen in einer anderen, ohne dass der Java-Code selbst etwas davon merkt.

Siehe auch: FileInputStream, FileOutputStream, BufferedReader, Files