4.02 Dateien lesen und schreiben

Einführung

In vielen Anwendungen ist es sehr nützlich – oder sogar nötig – Dateien einlesen und schreiben zu können. Zum Beispiel könnte man einen Vokabeltrainer erstellen, bei dem die Vokabeln in einer Textdatei gespeichert werden. Zusätzlich könnte man den Lernstand in einer Datei speichern, so dass man nicht bei jedem Start wieder von vorne mit dem Lernen beginnt.

Wir sehen uns daher hier an, wie wir mit Dateien in Java umgehen können. Wir konzentrieren uns dabei zunächst auf Textdateien.

Die Klasse File

Pfade und Auslesen von Informationen

Für den Umgang mit Dateien steht uns das Paket java.io zur Verfügung. Als erste Klasse aus diesem Paket ist die Klasse File zu nennen.

Ein Objekt der Klasse File repräsentiert einen Pfad, d.h., den Weg zu einer Datei oder zu einem Verzeichnis. Dies kann für uns anfangs etwas kniffelig sein. Zum einen, weil man in Zeiten von Betriebssystemen mit grafischer Benutzerüberfläche selten per Hand solche Pfade eingibt. Zum anderen, weil diese Pfade bei verschiedenen Betriebssystemen unterschiedlich aussehen können.


Hintergrundinfo

In Windows beginnt ein (absoluter) Pfad stets mit einem Laufwerksbuchstaben und einem Doppelpunkt. Für Gewöhnlich ist das Hauptlaufwerk C:. Auf diesem befindet sich das Betriebssystem und die Software. Andere Dateien legt man gerne auf separaten Laufwerken ab. Ein Beispiel für ein Verzeichnis wäre D:\Informatik\Java\. Ein Beispiel für eine Datei könnte dies sein: D:\Informatik\Java\Beispiel.java

In Linux gibt es keine Laufwerksbuchstaben. Stattdessen beginnt jeder Pfad mit der Wurzel /. Hier könnte zum Beispiel dies ein Verzeichnis sein: /home/daniel/Java. Der Pfad zu einer Datei sähe so aus: /home/daniel/Java/Beispiel.java.

Da ich noch nie an einem Mac saß, schreibe ich dazu erst einmal noch nichts. Wer sich damit auskennt, kann gerne unten einen Kommentar ergänzen 😉


Anstatt nun sofort zu versuchen, eine Datei zu öffnen, sehen wir uns zunächst einige weitere nützliche Operationen von File an. Meiner Erfahrung nach, kann es nämlich einige Nerven kosten, den richtigen Pfad auszutüfteln, besonders, wenn man auch von einer fertigen jar-Datei – also einer eigenständigen Anwendung – auf eine Datei zugreifen möchte.

In diesem ersten Beispiel kann nach dem Start eine Pfadangabe eingegeben werden. Danach wird angezeigt, ob der Pfad existiert und ob es sich um eine Datei oder ein Verzeichnis handelt:

import java.io.File;
import java.util.Scanner;


public class DateiDemo {

  public static void main(String[] args) {

    // Pfad wird eingelesen.
    Scanner meinScanner = new Scanner(System.in);
    System.out.print("Bitte Pfad angeben: ");
    String dateiname = meinScanner.next();
    meinScanner.close();

    // Instanz von File wird erzeugt.
    File meineDatei = new File(dateiname);
    
    // Ausgaben:
    System.out.println("Pfad vorhanden: "+meineDatei.exists());
    
    if (meineDatei.exists()){
      System.out.println("Es ist eine Datei: "+meineDatei.isFile());
      System.out.println("Es ist ein Verzeichnis "+meineDatei.isDirectory());
    }


  }

}

Unter Windows könntest Du dieses Beispiel so testen:

Bitte Pfad angeben: C:
Pfad vorhanden: true
Es ist eine Datei: false
Es ist ein Verzeichnis true

Ein weiterer Test unter Windows:

Bitte Pfad angeben: C:\Windows
Pfad vorhaden: true
Es ist eine Datei: false
Es ist ein Verzeichnis true

Unter Windows wird übrigens Groß- und Kleinschreibung ignoriert. Daher hätte c:\WINDOWS zum gleichen Ergebnis geführt. Da aber unter Linux Groß- und Kleinschreibung beachtet wird, sollten wir uns angewöhnen, immer auf diese zu achten – wir wollen ja nicht, dass unsere Programme nur unter Windows fehlerfrei laufen!

Hier ein Test unter Linux:

Bitte Pfad angeben: /home/Daniel
Pfad vorhanden: false

Wegen falscher Groß- und Kleinschreibung wurde mein persönliches Verzeichnis nicht gefunden.

Hier wird eine Datei die ich zur Übung dort abgelegt habe gefunden:

Bitte Pfad angeben: /home/daniel/text.txt
Pfad vorhanden: true
Es ist eine Datei: true
Es ist ein Verzeichnis false

Es kann durchaus hilfreich sein, zur Übung ein wenig nach verschiedenen Dateien oder Verzeichnissen zu suchen.

In diesem Beispiel haben wir schon drei Methoden der Klasse File im Einsatz gesehen. Hier ist eine Auswahl von einigen nützlichen Methoden – es gibt aber noch zahlreiche weitere. Es handelt sich dabei um Instanzmethoden:

Rückgabewert & Name Funktion
boolean exists() Liefert true, falls der angegebene Pfad existiert, ansonsten liefert sie false.
boolean isFile() Liefert true, falls der angegebene Pfad eine Datei beschreibt, ansonsten liefert sie false.
boolean isDirecory() Liefert true, falls der angegebene Pfad ein Verzeichnis beschreibt, ansonsten liefert sie false.
String getName() Liefert den Namen der Datei ohne die vorangehenden Pfad.
String getParent() Liefert das Oberverzeichnis als String.
File getParentFile() Liefert das Oberverzeichnis als File-Objekt.
File[] listFiles() Liefert ein Array, das alle Pfadnamen im Verzeichnis enthält, das durch das aktuelle File-Objekt beschrieben wird.

Die letzte Methode sehen wir uns in der folgenden Erweiterung unseres Beispiels an:

import java.io.File;
import java.util.Scanner;


public class DateiDemo {

  public static void main(String[] args) {

    // Pfad wird eingelesen.
    Scanner meinScanner = new Scanner(System.in);
    System.out.print("Bitte Pfad angeben: ");
    String dateiname = meinScanner.next();

    // Instanz von File wird erzeugt.
    File meineDatei = new File(dateiname);
    meinScanner.close();

    // Ausgaben:
    System.out.println("Pfad vorhanden: "+meineDatei.exists());

    if (meineDatei.exists()){
      System.out.println("Es ist eine Datei: "+meineDatei.isFile());
      System.out.println("Es ist ein Verzeichnis "+meineDatei.isDirectory());

      // Falls ein Verzeichnis vorliegt, wird sein Inhalt aufgelistet.
      if (meineDatei.isDirectory()){
        
        File[] inhalt = meineDatei.listFiles();
        
        for (int i=0; i<inhalt.length; i=i+1){
          System.out.println(inhalt[i]);
        }
      }
    }

  }

}

Falls der Pfad existiert und sich auf ein Verzeichnis bezieht, wird nun ein Array erstellt, dass alle darin enthaltenen Pfade (Verzeichnisse und Dateien) enthält. Anschließend wird es ausgegeben. Auch hier lohnt es sich, ein wenig damit zu experimentieren!

Anlegen und Löschen einer Datei

Die Klasse File bietet neben den Methoden von oben auch noch die Möglichkeit, eine neue Datei anzulegen. Für uns ungewohnt ist, dass die entsprechenden Instanzmethoden gleichzeitig einen boolean-Wert zur Bestätigung der Operation liefern:

Rückgabewert & Name Funktion
boolean createNewFile() Falls die Datei mit dem angegebenen Pfad und Namen noch nicht existiert, wird sie angelegt und der Wert true wird geliefert. Sonst wird false geliefert.
boolean delete() Falls die Datei mit dem angegebenen Pfad und Namen noch existiert, wird sie gelöscht und der Wert true wird geliefert. Sonst wird false geliefert.

Sehen wir uns ein Beispiel an:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;


public class DateiDemo {

  public static void main(String[] args){

    // Pfad wird eingelesen.
    Scanner meinScanner = new Scanner(System.in);
    System.out.print("Bitte Pfad angeben: ");
    String dateiname = meinScanner.next();

    // Instanz von File wird erzeugt.
    File meineDatei = new File(dateiname);
    meinScanner.close();

    System.out.println("Datei vorhanden: "+meineDatei.exists());
    System.out.println("Ist Datei: "+meineDatei.isFile());

    try{
      // Gab es die Datei bisher nicht, wird sie nun angelegt.
      System.out.println("Angelegt: "+meineDatei.createNewFile());

      // Hier wird sie zur Demonstration gelöscht.
      System.out.println("Gelöscht: "+meineDatei.delete());

      // Hier wird sie garantiert neu angelegt.
      System.out.println("Neu angelegt: "+meineDatei.createNewFile());

    } catch(IOException e){
      // Beispielsweise eine falsche Pfadangabe kann einem Fehler führen.
      System.out.println("Ein Fehler trat auf: "+e);
    }

  }
}

Beim Testen dieser Methode solltest Du darauf achten, keine Datei zu löschen, die Du noch benötigst!

FileReader und BufferdReader – Auslesen einer Textdatei

Klassisch mit try

Nun ist es aber an der Zeit, dass wir tatsächliche den Inhalt einer Datei auslesen. Sehen wir uns dies in der nächsten Erweiterung des Beispiels an. Danach gehen wir genauer auf Details ein:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Scanner;


public class DateiDemo {

  public static void main(String[] args) throws IOException{

    // Pfad wird eingelesen.
    Scanner meinScanner = new Scanner(System.in);
    System.out.print("Bitte Pfad angeben: ");
    String dateiname = meinScanner.next();
    meinScanner.close();

    // Instanz von File wird erzeugt.
    File meineDatei = new File(dateiname);

    // Ausgaben:
    System.out.println("Pfad vorhanden: "+meineDatei.exists());

    if (meineDatei.exists()){

      // Falls ein Verzeichnis vorliegt, wird sein Inhalt aufgelistet.
      if (meineDatei.isDirectory()){
        System.out.println("Inhalt des Verzeichnisses:");

        File[] inhalt = meineDatei.listFiles();

        for (int i=0; i<inhalt.length; i=i+1){
          System.out.println(inhalt[i]);
        }
      }else{		
        
        BufferedReader meinReader = null;
        
        // Liegt eine Datei vor, wird vefsucht, ihr Inhalt zu lesen.
        try{
          System.out.println("Inhalt der Datei:");
          
          // File- und Buffered-Reader werden erstellt und mit der Datei verbunden.
          meinReader = new BufferedReader(new FileReader(dateiname));
          String aktuelleZeile;
          
          // Die erste Zeile wird gelesen.
          aktuelleZeile = meinReader.readLine();

          // Ausgabe der Zeile und Einlesen der folgenden.
          while(aktuelleZeile != null){
            System.out.println(aktuelleZeile);
            aktuelleZeile = meinReader.readLine();
          }
          

        } finally{
          
          if(meinReader != null){
            meinReader.close();
          }
          
        }
      }
    }
  }
}

Zunächst sollte uns auffallen, dass unsere main-Methode eine IOException werfen kann. Das liegt zum einen daran, dass wir hier mögliche Fehler nicht mit einem catch abfangen und zum anderen auch an der Anweisung im finally-Block. Diese kann nämlich ebenfalls einen Fehler verursachen.

Das bringt uns direkt zu der Beobachtung, dass das Auslesen der Datei in einem try-Block stattfindet. Das muss so sein, da bei diesem Prozess eine IOException auftreten kann, z.B., weil die Datei beschädigt ist. Mit finally können wir hier sicherstellen, dass auch bei einem Fehler der Reader noch geschlossen wird. Darauf sollten wir nämlich immer achten!

Die Form der Anweisung BufferedReader meinReader = new BufferedReader(new FileReader(dateiname)); ist ein wenig ungewohnt.
Hier geschehen zwei Dinge. Zunächst wird ein File-Reader erstellt. Ein solcher ist – wie der Name sagt – dazu da, eine Datei zu lesen. Die entsprechende Datei wird ihm in Form des Pfades übergeben. Das kann wie ihr als String geschehen oder als Instanz von File.
Das Einlesen erledigt er Zeichen für Zeichen, was einerseits ineffizient sein kann und andererseits den Umgang für uns etwas umständlich gestalten kann. Daher empfiehlt es sich oft, einen solchen File-Reader in einen Buffered-Reader zu packen. Genau das geschieht in dieser Anweisung. Der Buffered-Reader erweitert die Funktionalität, so dass beispielsweise eine Textdatei zeilenweise gelesenen werden kann. Das zeilenweise Einlesen erreichen wir mit der Methode readLine(), die als Ergebnis einen String liefert.

In unserem Beispiel wird zunächst die erste Zeile eingelesen. In der Schleife wird dann geprüft, ob die Zeile noch einen Inhalt hatte. In diesem Fall wird sie ausgegeben und die nächste Zeile wird eingelesen.

Schließlich wird der Reader im finally-Block geschlossen.

Moderner mit try-with-resources

Wir sehen schon, dass dieses Vorgehen zum Umgang mit den möglichen Fehlern ein wenig umständlich aussieht. Eleganter ist eine Verwendung von try-with-resources. Beachte genau den Beginn des try-Blocks und das Wegfallen des finally:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Scanner;


public class DateiDemo {

  public static void main(String[] args) throws IOException{

    // Pfad wird eingelesen.
    Scanner meinScanner = new Scanner(System.in);
    System.out.print("Bitte Pfad angeben: ");
    String dateiname = meinScanner.next();
    meinScanner.close();

    // Instanz von File wird erzeugt.
    File meineDatei = new File(dateiname);

    // Ausgaben:
    System.out.println("Pfad vorhanden: "+meineDatei.exists());

    if (meineDatei.exists()){

      // Falls ein Verzeichnis vorliegt, wird sein Inhalt aufgelistet.
      if (meineDatei.isDirectory()){
        System.out.println("Inhalt des Verzeichnisses:");

        File[] inhalt = meineDatei.listFiles();

        for (int i=0; i<inhalt.length; i=i+1){
          System.out.println(inhalt[i]);
        }
      }else{		
        
        // try-with-resources funktioniert ab Java 7:
        try(BufferedReader meinReader = new BufferedReader(new FileReader(dateiname))){
          System.out.println("Inhalt der Datei:");
                    
          String aktuelleZeile;
          
          // Die erste Zeile wird gelesen.
          aktuelleZeile = meinReader.readLine();

          // Ausgabe der Zeile und Einlesen der folgenden.
          while(aktuelleZeile != null){
            System.out.println(aktuelleZeile);
            aktuelleZeile = meinReader.readLine();
          }
          
        // Der Reader wird nun automatisch geschlossen!

        } 
      }
    }
  }
}

Schreiben einer Textdatei

Das Schreiben in eine Textdatei funktioniert im Grunde analog zum Auslesen aus einer solchen:

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;


public class DateiDemo {

  public static void main(String[] args){

    // Pfad wird eingelesen.
    Scanner meinScanner = new Scanner(System.in);
    System.out.print("Bitte Pfad angeben: ");
    String dateiname = meinScanner.next();

    // Instanz von File wird erzeugt.
    File meineDatei = new File(dateiname);
    meinScanner.close();

    System.out.println("Datei vorhanden: "+meineDatei.exists());
    System.out.println("Ist Datei: "+meineDatei.isFile());

    // try-with-resources
    try(BufferedWriter meinWriter = new BufferedWriter(new FileWriter(meineDatei))){
      // Gab es die Datei bisher nicht, wird sie automatisch angelegt.
      System.out.println("Datei vorhanden: "+meineDatei.exists());

      // Mit einer Schleife werden ein paar Zeilen geschrieben.
      for(int i=1; i<10; i=i+1){
        meinWriter.write("Das ist die "+i+".Zeile!");
        meinWriter.newLine();
      }
      
      System.out.println("Schreibvorgang beendet!");

    } catch(IOException e){
      // Beispielsweise eine falsche Pfadangabe kann einem Fehler führen.
      System.out.println("Ein Fehler trat auf: "+e);
    }

  }
}

Mit write wird Text hinzugefügt und mit newline wird in eine neue Zeile gewechselt.