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