Java I/O Einführung
Überblick über Java I/O: Byte- vs. Zeichenströme, gepuffertes I/O, java.io vs. java.nio.file.
Teil 12 endete mit einem Vokabular, das sich direkt in diesen Teil mitnehmen lässt: Lambdas, Consumer<T> und Supplier<T> als Formen hinter „gib mir eine Zeile" und „mach etwas mit dieser Zeile", try-with-resources für alles, das eine deterministische Bereinigung benötigt, und die Stream-Pipeline für zeilenorientierte Daten. Javas I/O-APIs wurden genau um diese Formen herum konzipiert — lange bevor der Begriff „functional interface" existierte, hatten die zugrunde liegenden Objekte bereits je eine Methode, und das Post-Java-8-Fassade brachte den Rest.
Dieser Teil behandelt vier überlappende Toolkits:
java.io-Ströme — die ursprüngliche Java-1.0-API:InputStream/OutputStreamfür Bytes,Reader/Writerfür Zeichen sowie dieBuffered*-,Data*- undPrint*-Dekoratoren, die sie umhüllen.java.io.File— die veraltete Klasse „dieser String ist ein Pfad". Noch immer allgegenwärtig in älterem Code; für neue Arbeiten abgelöst durchjava.nio.file.Path.java.nio.file— die moderne API (Java 7+):Path,Filesund die statischen Hilfsmethoden (Files.readString,Files.writeString,Files.lines,Files.walk), die die meisten Dateioperationen auf eine Zeile reduzieren.- Serialisierung — Objektgraphen mit
ObjectOutputStream/ObjectInputStreamin Bytes umwandeln und zurück.
Die ersten sechs Kapitel arbeiten sich durch die Hochebenen-Dateioperationen (öffnen, erstellen, lesen, schreiben, löschen) mit beiden APIs, java.io und java.nio.file, damit dieselbe Aufgabe auf zwei Arten sichtbar wird. Die mittleren Kapitel zoomen in die Stream-Klassen selbst — Bytes vs. Zeichen, Pufferung, Daten, Druck. Die letzten Kapitel behandeln Serialisierung und die Path/Walk-API.
Die Byte/Zeichen-Trennung
Jede I/O-API in java.io hat eine von zwei Formen:
InputStream / OutputStream — byte-oriented (raw bytes: int read() returns 0..255 or -1)
Reader / Writer — character-oriented (decoded text: int read() returns a char or -1)Die Trennung ist nicht kosmetisch. Bytes sind das, was Festplatten und Sockets speichern; Zeichen sind das, was Menschen lesen. Eine .png-Datei sind Bytes; eine .txt-Datei sind ebenfalls Bytes auf der Festplatte, aber normalerweise möchte man sie mit einem Zeichensatz in Zeichen dekodiert haben. Die beiden ohne einen Zeichensatz zu vermischen ist die mit Abstand häufigste Quelle von „seltsamen Zeichen"-Fehlern in Legacy-Java-Code.
Die Brückenklassen — InputStreamReader und OutputStreamWriter — konvertieren zwischen den beiden und nehmen ein Charset-Argument entgegen. Verwende StandardCharsets.UTF_8, sofern kein dokumentierter Grund vorliegt, etwas anderes zu verwenden; die Formen ohne Argument verwenden den Plattformstandard, der sich je nach Betriebssystem unterscheidet und die Lehrbuchquelle für „funktioniert auf meinem Mac, kaputt auf dem Linux-Server"-Fehler ist.
Das Dekorator-Muster
java.io basiert auf dem Dekorator-Muster: eine kleine Menge von Rohströmen (FileInputStream, FileOutputStream, FileReader, FileWriter) wird in geschichtete Funktionalität eingewickelt (Pufferung, zeilenweiser Text, primitive Typen, formatierte Ausgabe). Du setzt am Aufrufpunkt zusammen, was du brauchst:
// Read a UTF-8 text file line by line:
try (BufferedReader in = new BufferedReader(
new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
}Drei Ebenen, von unten nach oben: FileInputStream liest rohe Bytes; InputStreamReader dekodiert sie als UTF-8-Zeichen; BufferedReader fügt einen In-Memory-Puffer und die Methode readLine() hinzu. Jede Ebene ist eine separate Klasse mit einer Aufgabe. Java 11 vereinfachte dieses genaue Muster auf eine Zeile — Files.newBufferedReader(path) — aber die Dekoration passiert immer noch darunter.
try-with-resources ist die Regel
Jeder Stream, Reader, Writer und Kanal in java.io und java.nio implementiert AutoCloseable. Das Schließen ist wichtig: ein nicht geschlossener FileOutputStream kann seinen Pufferschwanz verlieren; ein nicht geschlossener Socket leckt einen Dateideskriptor; ein nicht geschlossener Reader unter Windows hält eine Sperre, die das Betriebssystem nicht freigibt. Das try-with-resources-Konstrukt (Java 7+) garantiert, dass close() auf jedem erfolgreichen und fehlerhafte Pfad aufgerufen wird:
try (BufferedReader in = Files.newBufferedReader(path)) {
return in.readLine();
} // close() runs here, even if readLine() throwsDu kannst mehr als eine Ressource im selben try deklarieren; sie werden in umgekehrter Reihenfolge geschlossen. Älterer try/finally-Code, der close() manuell aufruft, ist fast jedes Mal falsch — die innere Ausnahme schluckt die close-Ausnahme, oder das Schließen wird auf dem Fehlerpfad vergessen. Verwende try-with-resources für alles, was ein Handle öffnet.
java.io vs. java.nio.file
java.io.File (1996) modellierte einen Pfad als String und bot eine Handvoll Operationen (exists, isFile, delete, listFiles). Die Klasse ist immer noch allgegenwärtig in Legacy-Code, und viele APIs geben noch File zurück oder akzeptieren es. Aber sie hat Grenzen, die das JDK nicht mehr leugnet:
- Keine Möglichkeit zu fragen, warum eine Operation fehlschlug —
file.delete()gibtfalsezurück für „die Datei existiert nicht", „Zugriff verweigert" und „die Datei ist offen". Man kann nicht unterscheiden, welches. - Keine Unterstützung für symbolische Links, Dateiattribute, Berechtigungen oder atomare Operationen.
- Keine Möglichkeit, einen Verzeichnisbaum zu durchlaufen, ohne die Rekursion selbst zu schreiben.
java.nio.file (Java 7) ersetzt es. Path ist der neue Typ „das ist ein Pfad", und Files ist eine static-Hilfsklasse mit rund 80 Methoden für alles, was man damit tun möchte:
Path p = Path.of("data", "users.txt"); // platform-independent path
String text = Files.readString(p, StandardCharsets.UTF_8); // whole file, one call
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "hello\n", StandardCharsets.UTF_8);
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
s.filter(l -> !l.isBlank()).forEach(System.out::println);
}Zwei Dinge zu beachten. Erstens gibt Files.lines(path) einen Stream<String> zurück — die Stream-Pipeline aus Teil 12 liest Dateien direkt. Zweitens besitzt der Stream ein offenes Datei-Handle, sodass der try-with-resources-Wrapper erforderlich ist — ohne ihn bleibt die Datei bis zum nächsten GC geöffnet.
Im gesamten Teil 13 zeigen wir beide APIs nebeneinander. Neuer Code sollte zuerst auf java.nio.file zurückgreifen; die Legacy-Kapitel existieren, weil du die älteren Formen in jeder Codebasis älter als Java 11 antreffen wirst.
Wohin dieser Teil führt
- Das nächste Kapitel, Java File Class, geht durch die veraltete
java.io.File-API — ihre Abfragemethoden, Auflistung und die Grenzen, diejava.nio.filemotiviert haben. - Die vier darauf folgenden Kapitel (Creating Files, Reading Files, Writing Files, Deleting Files) behandeln die Hochebenen-Operationen „tue eine Sache mit einer Datei" mit beiden APIs.
- Kapitel über Byte-, Zeichen- und gepufferte Ströme zoomen dann in den zugrunde liegenden
java.io-Dekorator-Stack. - Serialisierung, dann
Path,Filesund die Verzeichnis-Walk-API schließen den Teil ab.
Ein ausgearbeitetes Beispiel: dieselbe Aufgabe, vier Wege
Das folgende Programm schreibt eine kurze Textdatei auf vier Arten — einmal mit dem modernen Files.writeString, einmal mit dem klassischen FileWriter + try-with-resources, einmal mit BufferedWriter als Dekorator und einmal mit PrintWriter für formatierte Ausgabe. Dann liest es die Datei zweimal zurück — einmal mit Files.readString (ganze Datei, ein Aufruf) und einmal mit Files.lines als Stream<String>, gefiltert mit einem Predicate<String>. Das Beispiel verwendet eine temporäre Systemdatei, sodass es in jeder Sandbox funktioniert.
Was man dem Lauf entnehmen kann:
- Dieselbe Datei wurde auf vier verschiedene Arten geschrieben.
Files.writeStringist der kürzeste Weg für „lege diesen String in diese Datei";FileWriterist der klassische rohe Writer;BufferedWriterfügt einen In-Memory-Puffer hinzu (günstig, wenn man viele kleine Blöcke schreibt);PrintWriterfügtprintfhinzu. Jede überschrieb den vorherigen Inhalt, da der Standard-Öffnungsmodus „kürzen und dann schreiben" ist — man mussStandardOpenOption.APPENDübergeben (im Schreib-Kapitel behandelt), um zu einer Datei hinzuzufügen. - Jeder Writer lief innerhalb von
try-with-resources. Das Weglassen bei einem gepufferten Writer ist der Fehler, bei dem die letzten paar Zeichen nie auf die Festplatte gelangen —close()ist das, was den Pufferschwanz leert. Files.readStringgab die gesamte Datei als einStringzurück — für kleine Dateien in Ordnung, für ein 4-GB-Protokoll die falsche Wahl.Files.linesgab einenStream<String>zurück, den man durchfilter,mapundcountleiten kann, ohne die gesamte Datei im Speicher zu halten. Das erforderlichetry-with-resources auf dem Stream liegt daran, dass der Stream ein offenes Datei-Handle besitzt.- Die Zeile
Predicate<String> nameLine = l -> l.startsWith("name")ist dasselbe Vokabular aus Teil 12 — einPredicate-Wert, der anStream.filterübergeben wird.Files.linesist dort, wo die Streams-API auf die I/O-API trifft. Files.deleteIfExistsist das Löschen ohne Ausnahme: gibttruezurück, wenn die Datei entfernt wurde,false, wenn sie nicht vorhanden war. Das veralteteFile.delete()gibt einbooleanzurück für sowohl „gelöscht" als auch „konnte nicht löschen" —Filesunterscheidet die beiden durch Werfen einer Ausnahme.
Was kommt als Nächstes
Bevor die moderne java.nio.file-API den Rest des Teils übernimmt, behandelt das nächste Kapitel die Klasse, auf die man in jeder älteren Codebasis zuerst stößt: Java File Class. Sie ist der veraltete Pfad-und-Metadaten-Typ — begrenzt, aber allgegenwärtig — und zu sehen, was sie nicht kann, ist das, was den Fall für Path und Files macht.