09.04 Datenzugriffe auf das Dateisystem
Sicherlich haben Sie sich schon lange gefragt, wie Sie mit Java denn Daten auf die Festplatte schreiben können. Einen kleinen Einblick haben Sie bereits im Einstiegskapitel 09.01 Streams – Datenfluss in Java erhalten. Nun wollen wir dieses Wissen ein wenig vertiefen.
Text auf die Festplatte schreiben
Die Basisklasse für Textausgaben auf der Festplatte ist der java.io.FileWriter
. Beim Erzeugen eines neuen FileWriters
legen Sie sofort fest, in welche Datei der Writer schreiben soll. Um bspw. Text in die Datei „test.txt“ im Verzeichnis „C:\“ zu schreiben (unabhängig, ob die Datei bereits existiert oder nicht), können Sie den FileWriter
wie folgt initialisieren:
FileWriter fw = new FileWriter("C:\\test.txt");
Hierbei gibt es jedoch einige Punkte zu beachten.
- Bei der Initialisierung des
FileWriters
kann es zu einerIOException
kommen. Bspw. falls ein anderes Programm diese Datei sperrt. - Falls die Datei noch nicht vorhanden ist, wird sie neu angelegt, ansonsten überschrieben.
- Das Verzeichnis, in welchem die Datei erzeugt werden soll, muss existieren. Ansonsten wird eine
FileNotFoundException
geworfen. Wie Sie ein Verzeichnis anlegen, können Sie im Kapitel 09.02 Die Sicht auf das Dateisystem – java.io.File nachlesen. - Es ist sehr wichtig, dass Streams nach Ihrer Verwendung wieder geschlossen werden. Dies wird mit dem Aufruf der
close()
-Methode bewerkstelligt. Beim Aufruf dieser Methode werden alle Ressourcen des Streams (z. B. exklusive Dateizugriffe) wieder freigegeben. Falls dies nicht geschieht, gibt Java die Ressourcen spätestens beim Programmende wieder frei. - Wenn Sie einen Backslash im Dateinamen verwenden, beachten Sie, dass dieser maskiert werden muss. Alternativ können Sie einen normalen Slash (auch unter Windowssystemen) verwenden.
Wenn Sie die gerade genannten Punkte beachten, sieht eine saubere Initialisierung und Schließung eines Streams wie folgt aus:
FileWriter fw = null; try { fw = new FileWriter("C:/test.txt"); } catch (IOException e) { e.printStackTrace(); } finally { if (fw != null) try { fw.close(); } catch (IOException e) {} }
Das Schließen im finally
-Block ist mehr oder weniger standardisiert, so dass eine solche (unübersichtliche aber platzsparende) Schreibweise durchaus üblich ist.
Die eigentlichen Daten schreiben Sie dann mit einer der zahlreichen write
-Methoden. Hier ein Beispiel:
FileWriter fw = null; try { fw = new FileWriter("C:/test.txt"); fw.write("Hallo Welt!"); // normaler String fw.write('\n'); // Zeilenumbruch als char fw.write(65); // ASCII-Code für A fw.write('\n'); fw.write("Von mir wird nur ein Teil geschrieben", 5, 12); // Substring Funktionalität } catch (IOException e) { e.printStackTrace(); } finally { if (fw != null) try { fw.close(); } catch (IOException e) {} }
Beachten Sie, dass hier als Zeilenumbruch ein „\n“ verwendet wird. Nicht jeder Editor auf jedem Betriebssystem stellt ein „\n“ auch wirklich als Zeilenumbruch dar. Um dieses Problem zu umgehen, können Sie anstelle eines „\n“ folgenden Code verwenden:
System.getProperty("line.separator")
Dieser gibt den spezifischen Zeilenumbruch des jeweiligen Betriebssystems zurück. Das ist aber nur notwendig, wenn Sie davon ausgehen, dass die von Ihnen erzeugte Textdatei mit einem Editor und nicht (nur) von Ihrem Java-Programm gelesen wird.
Wenn Sie diesen Code ein weiteres Mal ausführen, steht nicht etwa der selbe Text zweimal in der Datei, sondern der bestehende Text wurde einfach durch den „neuen“ ersetzt. Manchmal kommt es jedoch vor, dass Sie eine Datei nicht komplett überschreiben, sondern lediglich weiteren Text hinten anhängen möchten. Ein Blick in die API-Dokumentation des FileWriters
offenbart diverse append
-Methoden. Probieren Sie eine solche aus.
FileWriter fw = null; try { fw = new FileWriter("C:/test.txt"); fw.write("Hallo Welt!"); fw.write(System.getProperty("line.separator")); fw.write(65); fw.write(System.getProperty("line.separator")); fw.write("Von mir wird nur ein Teil geschrieben", 5, 12); } catch (IOException e) { e.printStackTrace(); } finally { if (fw != null) try { fw.close(); } catch (IOException e) {} } try { fw = new FileWriter("C:/test.txt"); fw.append("Was passiert jetzt?"); } catch (IOException e) { e.printStackTrace(); } finally { if (fw != null) try { fw.close(); } catch (IOException e) {} }
Der Inhalt der neu erzeugten Datei sorgt jedoch für Ernüchterung: Der Text wurde nicht wie erwartet hinten angehängt. Aber verzweifeln Sie nicht. Es gibt dennoch eine Möglichkeit Text an eine existierende Datei anzuhängen. Und diese ist vermutlich sogar einfacher, als Sie es vermuten. Beim Erzeugen des FileWriters
geben Sie im Konstruktor noch einen weiteren Parameter mit an. Dieser (ein boolean
) spezifiziert, ob die Datei überschrieben (false
), oder erweitert (true
) werden soll. Dieser Wert steht standardmäßig auf false
.
FileWriter fw = null; try { fw = new FileWriter("C:/test.txt"); fw.write("Hallo Welt!"); fw.write(System.getProperty("line.separator")); fw.write(65); fw.write(System.getProperty("line.separator")); fw.write("Von mir wird nur ein Teil geschrieben", 5, 12); } catch (IOException e) { e.printStackTrace(); } finally { if (fw != null) try { fw.close(); } catch (IOException e) {} } try { fw = new FileWriter("C:/test.txt", true); // Daten anhängen fw.append("Was passiert jetzt?"); } catch (IOException e) { e.printStackTrace(); } finally { if (fw != null) try { fw.close(); } catch (IOException e) {} }
Text von der Festplatte einlesen
Nachdem Sie einen Text in einer Datei gespeichert haben, möchten Sie diesen selbstverständlich auch zu einem späteren Zeitpunkt wieder einlesen. Hierfür existiert in Java die Basisklasse java.io.FileReader
. Ihre Verwendung ist der des FileWriters
sehr ähnlich. Es existieren logischerweise lediglich read
anstelle von write
Methoden.
Generell gibt es mehrere Möglichkeiten eine Datei komplett auszulesen. Die erste beruht auf der Tatsache, dass die read
-Methoden des FileReaders
den Wert -1 zurückliefert, sobald das Ende der Datei erreicht ist. Ansonsten wird die Integer-Repräsentation des aktuellen Zeichens (bei jedem Aufruf von read
wird automatisch der interne Zeiger des FileReaders
um eine Stelle weiter nach vorne versetzt) der Datei zurückgeliefert. Mit diesem Wissen lässt sich eine simple Schleife programmieren, die eine Datei komplett einliest und bspw. in einer Zeichenkette abspeichert. Für diesen Vorgang verwenden wir aus Performancegründen einen StringBuilder
anstelle eines Strings
. Siehe hierzu Kapitel 03.08 StringBuffer und StringBuilder.
FileReader fr = null; try { fr = new FileReader("C:/test.txt"); StringBuilder content = new StringBuilder(); int tmp = 0; while ((tmp = fr.read()) != -1) { content.append((char)tmp); } System.out.println(content.toString()); } catch (IOException e) { e.printStackTrace(); } finally { if (fr != null) try { fr.close(); } catch (IOException e) {} }
Eine weitere und performantere Möglichkeit an den Inhalt einer Textdatei zu gelangen ist es, die Datei komplett über read(char[] cbuff)
in ein char
-Array zu lesen. Sie sparen sich hierdurch die Schleife und die temporäre Integer
Variable zum Zwischenspeichern des aktuellen Zeichens. Hierzu erzeugen Sie ein char
-Array mit der Größe der Datei (über ein File
-Objekt auslesbar) und übergeben dieses der read(char[] cbuff)
Methode. Einziger Nachteil ist, dass Sie keine Dateien einlesen können, die mehr Bytes beinhalten, als Sie ein Array groß erstellen können. Dies sollte aber eher selten bei Textdateien der Fall sein.
FileReader fr = null; File file = new File("C:/test.txt"); try { fr = new FileReader(file); char[] content = new char[(int)file.length()]; fr.read(content); System.out.println(String.valueOf(content)); } catch (IOException e) { e.printStackTrace(); } finally { if (fr != null) try { fr.close(); } catch (IOException e) {} }
Lese- und Schreibzugriffe puffern
Sie haben in Java ebenfalls die Möglichkeit das Einlesen und Schreiben von Texten zu puffern (dies funktioniert übrigens nicht nur bei Festplattenzugriffen, sondern generell bei allen Streams). Wenn Sie bspw. einen Schreibzugriff puffern, werden zuerst alle zu schreibenden Daten gesammelt, bis sie eine bestimmte Größe überschritten haben (frei definierbar). Erst dann werden die Zeichen alle auf einmal in die Datei geschrieben. Dadurch kann ein beachtlicher Performancegewinn erzielt werden. Beachten Sie aber, dass die Daten auch wirklich erst beim Schließen, oder wenn der Puffer voll ist, an den eigentlichen Stream weitergeleitet werden. Dadurch kann es sein, dass Sie Änderungen in bspw. einer Textdatei erst mit einer gewissen Verzögerung sehen. Möchten Sie dennoch die Daten im Puffer schreiben, bevor selbiger voll ist und ohne den Stream schließen zu müssen, rufen Sie flush()
auf. Das veranlasst den gepufferten Stream die gesammelten Daten sofort zu schreiben. Bei lesenden Operationen ist die Funktionsweise identisch – nur eben in die andere Richtung.
Um einen Puffer zu verwenden, verpacken Sie Ihre Reader
und Writer
in einem BufferedReader
bzw. BufferedWriter
.
FileReader fr = null; BufferedReader br = null; FileWriter fw = null; BufferedWriter bw = null; File file = new File("C:/test.txt"); try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write("Hallo Welt!"); bw.write(System.getProperty("line.separator")); bw.write(65); bw.write(System.getProperty("line.separator")); bw.write("Von mir wird nur ein Teil geschrieben", 5, 12); } catch (IOException e) { e.printStackTrace(); } finally { if (bw != null) try { bw.close(); } catch (IOException e) {} if (fw != null) try { fw.close(); } catch (IOException e) {} } try { fr = new FileReader(file); br = new BufferedReader(fr); char[] content = new char[(int)file.length()]; br.read(content); System.out.println(String.valueOf(content)); } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) try { br.close(); } catch (IOException e) {} if (fr != null) try { fr.close(); } catch (IOException e) {} }
Beachten Sie die Reihenfolge beim Schließen der Streams. Wenn Sie zuerst den FileWriter
schließen, bekommt der BufferedWriter
davon logischerweise nichts mit. Wird dann der BufferedWriter
geschlossen, versucht dieser die noch ausstehenden Zeichen an den FileWriter
weiterzuleiten, was aber nicht funktionieren kann, da dieser bereits geschlossen ist. Schließen Sie also immer zuerst den gepufferten Stream.
Zusätzlich bieten der BufferedReader
und der BufferedWriter
jeweils noch eine zusätzliche Komfortfunktion. Mit dem BufferedReader
können Sie den Text zeilenweise über readLine()
auslesen, und mit dem BufferedWriter
direkt eine neue Zeile über newLine()
schreiben.
FileReader fr = null; BufferedReader br = null; FileWriter fw = null; BufferedWriter bw = null; File file = new File("C:/test.txt"); try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write("Hallo Welt!"); bw.newLine(); bw.write(65); bw.newLine(); bw.write("Von mir wird nur ein Teil geschrieben", 5, 12); } catch (IOException e) { e.printStackTrace(); } finally { if (bw != null) try { bw.close(); } catch (IOException e) {} if (fw != null) try { fw.close(); } catch (IOException e) {} } try { fr = new FileReader(file); br = new BufferedReader(fr); String str = null; int lineCounter = 0;; while ((str = br.readLine()) != null) { System.out.println("Zeile " + ++lineCounter + ": " + str); } } catch (IOException e) { e.printStackTrace(); } finally { if (br != null) try { br.close(); } catch (IOException e) {} if (fr != null) try { fr.close(); } catch (IOException e) {} }