Java Dateien
Willemers Informatik-Ecke

Nachts, wenn es dunkel ist ...

Datei per Scanner einlesen

Unter java.util findet sich die Klasse Scanner. Scanner ist vielen Java-Programmierern als Möglichkeit bekannt, Daten auf die Schnelle von der Tastatur einzulesen.
java.util.Scanner eingabe = new java.util.Scanner(System.in);
double zahl = eingabe.nextDouble();
Der Scanner liest nicht nur den Standardeingabekanal, sondern auch Dateien.
java.io.File datei = new java.io.File("datei.txt");
if (datei.exists()) {
	java.util.Scanner eingabe;
	try {
		eingabe = new java.util.Scanner(datei);
		double zahl = eingabe.nextDouble();
Scannen Sie wohl! Scanner kennt folgende Methoden:

Wo ist die Datei?

Aber wo befindet sich die Datei datei.txt, die oben im Beispiel verwendet wird? Das Programm sucht die Datei in dem Verzeichnis, in dem es gestartet wurde.

Wurde es aus Eclipse gestartet, so schaut es im Wurzelverzeichnis des Projekts nach. Das ist sehr praktisch, weil man dort sehr einfach eine Datei aus der Eclipse-Oberfläche erstellen und dann gleich mit dem Editor bearbeiten kann.

Befindet sich die Datei an einer anderen Stelle, muss der komplette Pfad eingeben werden.

Bei Windows kommt noch der lästige Laufwerksbuchstabe hinzu. Abgesehen davon, dass Windows als Pfadtrenner aus den Backslash ausgewichen ist, weil MS-DOS ursprünglich keine Verzeichnisse kannte und den Schrägstrich bereits für Optionen verwendet hatte.

Der Backslash hat den Nachteil, dass er in Strings bereits als Escape-Zeichen verwendet wird. Darum muss für einen Backslash immer \\ eingegeben werden.

Inzwischen kann auch Windows /, Sie können also auch einen / verwenden. Ganz korrekt ist es, wenn Sie File.separator als portables Pfadtrennerzeichen verwenden.

Benutzer suchen lassen

Es gibt Dateiauswahlboxen! Die haben nicht nur den Vorteil, dass der Anwender das Programm für besonders benutzerfreundlich hält, sie liefern sogar das Datei-Handle, ohne dass sich der Programmierer um den Pfad kümmern muss.
import javax.swing.JFileChooser;
// ...

JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(null)==JFileChooser.APPROVE_OPTION) {
	datei = chooser.getSelectedFile();
}
try {
	java.util.Scanner eingabe = new java.util.Scanner(datei);
	double zahl = eingabe.nextDouble();

Ausnahmebehandlung

In kaum einem Bereich kann so viel in die Hose gehen, wie im IO-Bereich. Diese Fälle lösen eine Ausnahmesituation aus, die der Programmierer entsprechend behandeln soll. Es treten folgende Exceptions auf:
java.io.File datei = new java.io.File("datei.txt");
if (datei.exists()) {
	java.util.Scanner eingabe = null;
	try {
		eingabe = new java.util.Scanner(datei);
		while(eingabe.hasNext()) {
			double zahl = eingabe.nextDouble();
			System.out.println(zahl);
		};
	} catch (FileNotFoundException e) {
		System.out.println("Wo ist die Datei?");
	} catch (InputMismatchException e) {
		System.out.println("Das soll eine Zahl sein?");
	} catch (NoSuchElementException e) {
		// fertig gelesen. Da ist nichts mehr!
	} catch (Exception e) {
		// Gebe die Exception aus!
		e.printStackTrace();
	}
	if (eingabe!=null) {
		eingabe.close();
	}
}
Am Ende wird ein artiger Programmierer den Scanner mit close schließen.

Schreiben von Daten

Wenn man niemanden findet, der die Dateien per Editor eintippt, ist es gut, wenn man ein Programm schreiben kann, dass solche Dateien erzeugt. Wie in vielen wissenschaftlichen Studien üblich, erstellt auch dieses Beispielprogramm seine Ergebnisse auf der Basis eines verlässlichen Zufallsgenerators.
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;

public class Zahlenschreiben {
	public static void main(String[] args) {
	    Random random = new Random();
	    String linefeed = System.getProperty("line.separator");
		try {
			FileWriter datei = new FileWriter("datei.txt");

			for (int i=0; i<10; i++) {
				double wert = random.nextDouble();
				String strWert = "" + wert;
				strWert = strWert.replace('.', ',');
				datei.write(strWert+linefeed);
			}
			datei.close();
		} catch (IOException e) {			
		}
	}
}
Um Dateien zu schreiben, wird ein Objekt der Klasse FileWriter angelegt, das als Parameter den Dateinamen erhält.

Die Klasse stellt eine Methode write zur Verfügung, mit der sich Strings in Dateien schreiben lässt. Auch in diesem Fall sollte die Datei wieder mit close geschlossen werden.

Einschub: Block-Devices

Festplatten sind Block-Devices. Jeder Lese- und Schreibzugriff überträgt mehrere KByte an Daten. Bei kleinen Änderungen gibt es darum unnötigen IO. Da eine Festplatte ca 1000 Mal langsamer als der Hauptspeicher ist, verwendet man zum Schreiben RAM-Puffer. Schreibt Ihr Programm also größere Datenmengen, können Sie es erheblich beschleunigen, indem Sie BufferedWriter verwenden.

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;

public class Zahlenschreiben {
	public static void main(String[] args) {
	    Random random = new Random();
	    String linefeed = System.getProperty("line.separator");
		try {
			FileWriter fw = new FileWriter("datei.txt");
			BufferedWriter datei = new BufferedWriter(fw);
			for (int i=0; i<10; i++) {
				double wert = random.nextDouble();
				String strWert = "" + wert;
				strWert = strWert.replace('.', ',');
				datei.write(strWert+linefeed);
			}
			datei.close();
		} catch (IOException e) {			
		}
	}
}
Das Programm unterscheidet sich minimal von dem vorigen. Es wird lediglich ein Objekt der Klasse BufferedWriter auf der Basis des FileWriters erzeugt und dieses mit dem Schreiben in die Datei beauftragt.

FileReader und BufferedReader

Für das Lesen von Textdateien wurde oben bereits der Scanner vorgestellt. Es gibt aber auch ein Gegenstück zum FileWriter namens FileReader.

FileReader stellt die Methode read zur Verfügung, die aber nur ein Zeichen liefert. Analog zum BufferedWriter gibt es zum Lesen die Klasse BufferedReader, die die Methode readLine zur Verfügung stellt, die eine ganze Zeile liefert.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class DateiBufferedLesen {
    public static void main(String[] args) {
        BufferedReader datei;
        String zeile;
        try {
            datei = new BufferedReader(
                    new FileReader("datei.txt"));
            while ((zeile = datei.readLine()) != null) {
                System.out.println(zeile);
            }
            datei.close();
        } catch (IOException e) {
            System.err.println("Dateilesefehler");
        }
    }
}

Klassen in die Datei

Natürlich lassen sich alle möglichen Typen in Text und auch wieder zurück wandeln. Sollen aber Objekte persistent gehalten werden, wird der Aufwand schnell groß. Aber das muss nicht sein. Java kann helfen.

Klasse implementiert Serializable

Die zu sichernde Klasse muss serialisierbar sein. Um das zu gewährleisten, genügt es, der Klasse beizufügen, dass sie das Interface Serializable implementiert. Eine Implementierung von Methoden erzwingt das nicht.
import java.io.Serializable;

public class KlassenIO {

	static class Auto implements Serializable {
		String marke, modell;
		int hubraum, ps;
		double preis;
	}
	
	public static void main(String[] args) {
		Auto auto = new Auto();
		auto.marke = "Rollsreuss";
		auto.modell = "R4";
		auto.hubraum = 845;
		auto.ps = 280;
		auto.preis = 12879;

Schreiben des Objekts

Für einen FileOutputStream wird ein ObjectOutputStream angelegt. Dieser stellt die Methode writeObject zur Verfügung. Diese Methode sichert jedes serialisierbare Objekt.
	static void schreibeAuto(Auto auto) throws IOException {
		FileOutputStream fos = new FileOutputStream("auto.data");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(auto);
		oos.close();
	}

Objekt wieder einlesen

Um ein Objekt zu lesen, wird der Vorgang umgekehrt. Ein ObjectInputStream wird aus einem FileInputStream erzeugt. Die Methode readObject liefert ein serialisierbares Objekt, das vor der weiteren Verwendung gecastet werden muss.
	static Auto leseAuto() throws IOException {
		Auto auto = null;
		FileInputStream fis = new FileInputStream("auto.data");
		ObjectInputStream ois = new ObjectInputStream(fis);
		try {
			auto = (Auto) ois.readObject();
		} catch (ClassNotFoundException e) {
		}
		ois.close();
		return auto;
	}

Hauptprogramm

Da die Methoden IOException werfen, muss main sie fangen.
	public static void main(String[] args) {
		Auto auto = new Auto();
		auto.marke = "Rollsreuss";
		auto.modell = "R4";
		auto.hubraum = 845;
		auto.ps = 280;
		auto.preis = 12879;
		try {
			schreibeAuto(auto);
			Auto wagen = leseAuto();
			if (wagen!=null) {
				System.out.println(wagen.marke);
			}
		} catch (IOException e) {
		}
	}

Betrachtung der Dateiformate

Scanner, FileWriter und FileReader verarbeiten reine Textdateien.

FileInputStream und FileOutputStream verwenden spezielle Java-Dateien, um darin die Referenzen und binäre Daten zu kodieren.

Portable Dateiformate sind Textdateien, die von mehreren Programmiersprachen und Plattformen interpretierbar sind, wie XML oder JSON.

Wahlfreier Zugriff (Random Access)

Die bisherigen Dateizugriffe erfolgen sequentiell. Die Klasse RandomAccessFile ermöglicht das Positionieren an eine beliebige Position einer Datei und das Auslesen eines Blocks. Da für das Schreiben und Lesen der Blöcke eine konstante Speichergröße benötigt wird, erlauben read und write als Parameter nur ein Array von byte. Über dessen Größe wird dann auch die Größe des gelesenen oder geschriebenen Blocks definiert.

Man könnte diese Behandlung auch als "Array als Datei" betrachten. Solche wahlfreien Zugriffe sind die Basis einer Datenbank und können beispielsweise verwendet werden, um fremde Dateiformate zu lesen.

final int BLOCKSIZE = 1024;
final int OFFSET = 0;
int pos = 0;
byte[] block = new byte[BLOCKSIZE];
try {
    RandomAccessFile datei = new RandomAccessFile("test.dbf", "rw");
    datei.seek(pos * BLOCKSIZE + OFFSET);
    datei.write(block);
    pos++;
    datei.seek(pos * BLOCKSIZE + OFFSET);
    datei.read(block);
    datei.close();
} catch(FileNotFoundException e) {
} catch(IOException e) {
}