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.

  1. Bei der Initialisierung des FileWriters kann es zu einer IOException kommen. Bspw. falls ein anderes Programm diese Datei sperrt.
  2. Falls die Datei noch nicht vorhanden ist, wird sie neu angelegt, ansonsten überschrieben.
  3. 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.
  4. 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.
  5. 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) {}
}

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.