19.5 Lesen aus Dateien und Schreiben in Dateien
Um Daten aus Dateien lesen oder in Dateien schreiben zu können, ist eine Stromklasse nötig, die es schafft, die abstrakten Methoden von Reader, Writer, InputStream und OutputStream auf Dateien abzubilden. Um an solche Implementierungen zu kommen, gibt es drei verschiedene Ansätze:
Die Utility-Klasse Files bietet einige newXXX(…)-Methoden, die uns Lese/Schreib-Datenströme für zeichen- und byteorientierte Dateien liefern.
Ein Class-Objekt bietet getResourceAsStream(…) und liefert einen InputStream, der Bytes aus Dateien im Modulpfad liest. Zum Schreiben gibt es nichts Vergleichbares. Falls Unicode-Zeichen gelesen werden sollen, muss der InputStream in einen Reader konvertiert werden.
Die speziellen Klassen FileInputStream, FileReader, FileOutputStream, FileWriter sind Stromklassen, die read(…)/write(…)-Methoden auf Dateien abbilden. So gibt es weitere Klassen für spezielle Quellen und Senken, etwa Netzwerkverbindungen.
Jede der Varianten hat Vor- und Nachteile. Wir wollen die einzelnen Möglichkeiten nun kennenlernen und voneinander abgrenzen.
19.5.1 Byteorientierte Datenströme über Files beziehen
Die Files-Klasse bietet Methoden, die direkt den Eingabe-/Ausgabestrom liefern. Beginnen wir mit den byteorientierten Stream-Klassen:
final abstract java.nio.file.Files
static OutputStream newOutputStream(Path path, OpenOption... options)
throws IOException
Legt eine Datei an und liefert den Ausgabestrom auf die Datei.static InputStream newInputStream(Path path, OpenOption... options)
throws IOException
Öffnet die Datei und liefert einen Eingabestrom zum Lesen.
Da die OpenOption ein Vararg ist und somit weggelassen werden kann, ist der Programmcode kurz. (Er wäre noch kürzer ohne die korrekte Fehlerbehandlung …)
Beispiel: Eine kleine PPM-Grafikdatei schreiben
Das PPM-Format ist ein einfaches Grafikformat. Es beginnt mit einem Identifizierer, dann folgen die Ausmaße und schließlich die ARGB-Werte für die Pixelfarben.
try ( OutputStream out = Files.newOutputStream( Paths.get( "littlepic.tmp.ppm" ) ) ) {
out.write( "P3 1 1 255 255 0 0".getBytes( StandardCharsets.ISO_8859_1 ) );
}
catch ( IOException e ) {
e.printStackTrace();
}
PPM-Dateien können online konvertiert werden, etwa mit https://convertio.co/de/ppm-jpg/.
19.5.2 Zeichenorientierte Datenströme über Files beziehen
Neben den statischen Files-Methoden newOutputStream(…) und newInputStream(…) gibt es zwei Methoden, die zeichenorientierte Ströme liefern, also Reader/Writer:
static BufferedReader newBufferedReader(Path path, Charset cs)
throws IOExceptionstatic BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options)
throws IOException
Liefern einen Ein-/Ausgabestrom, der Unicode-Zeichen liest. Das Charset-Objekt bestimmt, in welcher Zeichenkodierung sich die Texte befinden, damit sie korrekt in Unicode konvertiert werden.static BufferedReader newBufferedReader(Path path)
throws IOException
Entspricht newBufferedReader(path, StandardCharsets.UTF_8).static BufferedWriter newBufferedWriter(Path path, OpenOption... options)
throws IOException
Entspricht Files.newBufferedWriter(path, StandardCharsets.UTF_8, options).
BufferedReader und BufferedWriter sind Unterklassen von Reader/Writer, die zum Zwecke der Optimierung Dateien im internen Puffer zwischenspeichern.
newBufferedWriter(…)
Die Rückgabe von newBufferedWriter(…) ist ein BufferedWriter, eine Unterklasse von Writer. Jeder Writer hat Methoden wie write(String), die Zeichenketten in den Strom schreiben. Die Methode soll das nächste Beispiel nutzen:
try ( Writer out = Files.newBufferedWriter( Paths.get( "out.bak.txt" ),
StandardCharsets.ISO_8859_1 ) ) {
out.write( "Zwei Jäger treffen sich ..." );
out.write( System.lineSeparator() );
}
catch ( IOException e ) {
e.printStackTrace();
}
newBufferedReader()
Der BufferedReader bietet neben den einfachen geerbten Lesemethoden der Oberklasse Reader zwei weitere praktische Methoden:
String readLine(): Liest eine Zeile bis zum Zeilenendezeichen (oder Stromende). Die Rückgabe ist null, wenn keine neue Zeile gelesen werden kann, weil das Stromende erreicht wurde. Das Zeilenendezeichen ist nicht Teil des Strings.
Stream<String> lines(): Liefert einen Stream von Strings, wobei jeder String eine Zeile (ohne den Trenner) repräsentiert.
So einfach ist ein Programm formuliert, das alle Zeilen einer Datei abläuft:
try ( BufferedReader in = Files.newBufferedReader( Paths.get( "lyrics.txt" ),
StandardCharsets.ISO_8859_1 ) ) {
for ( String line; (line = in.readLine()) != null; )
System.out.println( line );
}
catch ( IOException e ) {
e.printStackTrace();
}
[zB] Beispiel
Mit der Stream-API sieht es ähnlich aus – kurz skizziert:
try ( BufferedReader in = Files.newBufferedReader( … ) ) {
in.lines().forEach( System.out::println );
}
Falls es beim Lesen über den Stream zu einem Fehler kommt, wird eine RuntimeException vom Typ UncheckedIOException ausgelöst.
19.5.3 Die Funktion von OpenOption bei den Files.newXXX(…)-Methoden
Sofern eine Datei schon existiert, wird sie beim Öffnen zum Schreiben sozusagen gelöscht und dann neu beschrieben; existiert sie nicht, wird sie neu angelegt. Diese Standardoption ist aber ein wenig zu einschränkend, und daher beschreibt OpenOption Zusatzoptionen. OpenOption ist eine Schnittstelle, die von den Aufzählungen LinkOption und StandardOpenOption realisiert wird.
OpenOption | Beschreibung |
---|---|
java.nio.file.StandardOpenOption | |
READ | Öffnen für Lesezugriff |
WRITE | Öffnen für Schreibzugriff |
Neue Daten kommen an das Ende. Atomar bei parallelen Schreiboperationen. | |
TRUNCATE_EXISTING | Für Schreiber: Existiert die Datei, wird die Länge vorher auf 0 gesetzt. |
CREATE | Legt die Datei an, falls sie noch nicht existiert. |
CREATE_NEW | Legt die Datei nur an, falls sie vorher noch nicht existierte. Andernfalls gibt es einen Fehler. |
DELETE_ON_CLOSE | Die Java-Bibliothek versucht, die Datei zu löschen, wenn diese geschlossen wird.* |
SPARSE | Hinweis an das Dateisystem, die Datei kompakt zu speichern, da sie aus vielen Null-Bytes besteht.** |
SYNC | Jeder Schreibzugriff und jedes Update der Metadaten soll sofort zum Dateisystem. |
DSYNC | Jeder Schreibzugriff soll sofort zum Dateisystem. |
java.nio.file.LinkOption | |
NOFOLLOW_LINKS | Symbolischen Links wird nicht gefolgt. |
* Das ist zum Beispiel dann praktisch, wenn der Hauptspeicher zu klein ist und Dateien zum Lesen/Schreiben als Zwischenspeicher verwendet werden. Am Ende der Operation kann die Datei gelöscht werden. ** Das ist für Windows und NTFS interessant, siehe auch https://stackoverflow.com/questions/17634362/what-is-the-use-of-standardopenoption-sparse. |
Die Option CREATE_NEW kann nur funktionieren, wenn die Datei noch nicht vorhanden ist. Das zeigt anschaulich das folgende Beispiel:
Files.deleteIfExists( Paths.get( "opa.herbert.tmp" ) );
Files.newOutputStream( Paths.get( "opa.herbert.tmp" ) ).close();
Files.newOutputStream( Paths.get( "opa.herbert.tmp" ) ).close();
Files.newOutputStream( Paths.get( "opa.herbert.tmp" ),
StandardOpenOption.CREATE_NEW ).close();
Hier führt die letzte Zeile zu »java.nio.file.FileAlreadyExistsException: opa.herbert.tmp«.
Die Option DELETE_ON_CLOSE ist für temporäre Dateien nützlich. Das folgende Beispiel verdeutlicht die Arbeitsweise:
Path path = Paths.get( "opa.herbert.tmp" );
Files.deleteIfExists( path );
System.out.println( Files.exists( path ) ); // false
Files.newOutputStream( path ).close();
System.out.println( Files.exists( path ) ); // true
Files.newOutputStream( path, StandardOpenOption.DELETE_ON_CLOSE,
StandardOpenOption.SYNC ).close();
System.out.println( Files.exists( path ) ); // false
Im letzten Fall wird die Datei angelegt, ein Datenstrom geholt und gleich wieder geschlossen. Wegen StandardOpenOption.DELETE_ON_CLOSE wird Java die Datei von sich aus löschen, was Files.exists(Path, LinkOption...) belegt.
19.5.4 Ressourcen aus dem Modulpfad und aus JAR-Dateien laden
Im Klassen-/Modulpfad können neben den Klassendateien auch Ressourcen wie Grafiken oder Konfigurationsdateien enthalten sein. Der Zugriff auf diese Dateien wird nicht über Path oder File realisiert, denn die Dateien können sich innerhalb eines JAR-Archivs befinden. Daher gibt es am Class-Objekt getResourceAsStream(String):
final class java.lang.Class<T>
implements Serializable, GenericDeclaration, Type, AnnotatedElement
InputStream getResourceAsStream(String name)
Gibt einen Eingabestrom auf die Datei mit dem Namen name zurück oder null, falls es keine Ressource mit dem Namen im Modulpfad gibt oder die Sicherheitsrichtlinien das Lesen verbieten.
[»] Hinweis
Es geht bei der Methode nicht darum, eine Datei in einer anderen JAR-Datei zu öffnen und auszulesen.
Da der Klassenlader die Ressource findet, entdeckt er alle Dateien, die im Pfad des Klassenladers eingetragen sind. Das gilt auch für JAR-Archive, weil dort vom Klassenlader alles verfügbar ist. Da die Methode keine Ausnahme auslöst, ist ein Test auf die Rückgabe ungleich null unabdingbar.
Das folgende Programm liest ein Byte aus der Datei onebyte.txt ein und gibt es auf dem Bildschirm aus:
package com.tutego.insel.io.stream;
import java.io.*;
import java.util.Objects;
public class GetResourceAsStreamDemo {
public static void main( String[] args ) {
String filename = "onebyte.txt";
try ( InputStream is = Objects.requireNonNull(
GetResourceAsStreamDemo.class.getResourceAsStream( filename ),
"Datei gibt es nicht!" ) ) {
System.out.println( is.read() ); // 49
}
catch ( IOException e ) {
e.printStackTrace();
}
}
}
Die Datei onebyte.txt befindet sich im gleichen Pfad wie auch die Klasse, sie liegt also in com/tutego/insel/io/stream/onebyte.txt. Liegt die Ressource zum Beispiel im Wurzelverzeichnis des Pakets, lautet die Angabe "/onebyte.txt". Liegen die Ressourcen außerhalb des Modulpfades, können sie nicht gelesen werden. Der große Vorteil ist aber, dass die Methode alle Ressourcen anzapfen kann, die über den Klassenlader zugänglich sind, und das ist insbesondere der Fall, wenn die Dateien aus JAR-Archiven kommen – hier gibt es keinen üblichen Pfad im Dateisystem, er hört in der Regel beim JAR-Archiv selbst auf.
Um die getResourceAsStream(String)-Methoden nutzen zu können, ist ein Class-Objekt nötig, das wir in unserem Fall über Typname.class besorgen. Das ist nötig, weil unsere main(String[])-Methode statisch ist. Andernfalls kann innerhalb von Objektmethoden auch getClass() eingesetzt werden, eine Methode, die jede Klasse aus der Basisklasse java.lang.Object erbt.