12.1.1 Zielsetzung
In unserem Buch-Projekt hatten wir eine Klasse Buchliste, die eine ganze Reihe von Büchern in einer ArrayList<Buch> speicherte. Mit einer erzeugen()-Methode konnten wir dann Objekte der verschiedenen Unterklassen von Buch in dieser Liste speichern. Mit einer zeige()-Methode ließen sich die Bücher dann in der Konsole anzeigen.
public void erzeuge()
{
try
{
liste.add(new Sachbuch("Einführung in Java","Meier, Otto","Informatik", 2025));
liste.add(new Sachbuch("Lexikon der Informatik","Duden-Verlag","Informatik",2012));
// und so weiter ...
}
catch (IllegalArgumentException iae)
{
System.out.println("Fehler bei liste.erzeuge(): " + iae.getMessage());
}
}
public void zeige()
{
for (Buch b : liste)
b.zeige();
}
Wir wollen die Klasse Buchliste nun um eine Methode erweitern, mit der die Daten der Bücher aus einer Textdatei gelesen werden können. Diese Textdatei können wir selbst mit einem einfachen Texteditor erstellen. Dabei müssen wir lediglich darauf achten, dass
- Jede Zeile genau ein Buch enthält.
- Jede Zeile nach dem gleichen Schema aufgebaut ist.
12.1.2 Vorbereitung
Wie kann man überhaupt eine Textdatei zeilenweise auslesen und in der Konsole anzeigen? Das soll das folgende kleine Programm verdeutlichen:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class DateiLesen
{
public static void main(String[] args) throws IOException
{
String dateiname = "DateiLesen.java";
for (String zeile : Files.readAllLines(Path.of(dateiname)))
System.out.println(zeile);
}
}
Erklärung des Programms
Die importierten Klassen
import java.io.IOException;
Diese Anweisung importiert die Klasse IOException. Das ist notwendig, denn beim Lesen einer Datei kann einiges schiefgehen: Die Datei ist nicht vorhanden, kann nicht gelesen werden, es fehlen Zugriffsrechte oder es tritt ein Fehler beim Lesen der Festplatte auf. In solchen Fällen wird eine IOException ausgelöst.
import java.nio.file.Files;
Die Klasse Files enthält wichtige Methoden zum Lesen und Schreiben von Dateien. Einzelheiten dazu finden sich im Anhang an dieses Skript.
Die Abkürzung "nio" steht übrigens für "new Input/Output".
import java.nio.file.Path;
Path ist eine weitere wichtige Java-Klasse. Ein Objekt dieser Klasse beschreibt einen Datei- oder Verzeichnispfad, beispielsweise den Pfad zu einer Textdatei.
Der Kopf der main()-Methode
public static void main(String[] args) throws IOException
Hier sehen wir die übliche main()-Methode, allerdings mit dem Zusatz
throws IOException
Die IOException, die beim Lesen von Dateien auftreten kann, ist eine checked Exception, die zwingend behandelt oder an den Aufrufer (aufrufende Methode, main()-Methode oder JVM) weitergegeben werden muss. Eine Behandlung erfolgt normalerweise in einem try-catch-Block. Findet - wie in unserer Klasse - keine solche Behandlung statt, muss die Methode die Exception an den Aufrufer weiterleiten. Dies geschieht mit dem Schlüsselwort throws und der Angabe der Exception-Klasse.
String dateiname = "DateiLesen.java";
Hier wird der Name der zu öffnenden Datei festgelegt. Normalerweise geschieht dies über eine Benutzereingabe, beispielsweise über eine Dialogbox, ein Menü oder die Konsole. Um den Quelltext unseres ersten Beispiels möglichst einfach zu halten, legen wir den Dateinamen direkt im Programm fest.
Eine Pfadangabe ist nicht nötig, weil sich die Datei DateiLesen.java im gleichen Verzeichnis befindet wie das Java-Projekt. Es handelt sich nämlich um die Textdatei mit dem Quelltext dieser Klasse.
for (String zeile : Files.readAllLines(Path.of(dateiname)))
System.out.println(zeile);
Diese for-each-Schleife durchläuft alle Zeilen der Textdatei nacheinander.
Path.of(dateiname) erzeugt aus dem Dateinamen ein Path-Objekt, das den Pfad zur Datei beschreibt.
Files.readAllLines(...) liest sämtliche Zeilen der Datei ein und liefert sie als Liste von Strings zurück.
String zeile bedeutet: In jedem Schleifendurchlauf wird der Variable zeile das jeweils nächste Element dieser Liste zugewiesen. Jedes Element der Liste entspricht genau einer Zeile der eingelesenen Textdatei.
Mit System.out.println(zeile) wird dieses Listenelement (die Zeile) schließlich in der Konsole angezeigt.
12.1.3 Anwendung auf das Buch-Projekt
Die Textdatei mit den Büchern
Wir lassen uns von einer KI eine Textdatei mit vielen Büchern erstellen, wobei jede Zeile für ein Buch steht. Betrachten wir einen Ausschnitt aus einer solchen Textdatei:
Roman,Karl May,Winnetou I,1890,Abenteuerroman Lehrbuch,Michael Kölling,Java lernen mit BlueJ,2017,Informatik,ANFAENGER Lexikon,Redaktion Duden,Fremdwörterbuch,2020,Sprache,5500↲
Die einzelnen Datenfelder der Bücher sind durch Kommas getrennt, die einzelnen Bücher durch Zeilenumbrüche - in diesem Beispiel durch das Symbol ↲ dargestellt.
Hier eine Übersicht über die Datenfelder:
- Art des Buches: Roman, Lehrbuch oder Lexikon
- Name des Autors (Vorname und Name bilden ein Feld!)
- Titel des Buchs
- Erscheinungsjahr des Buchs
- Bei Romanen: Das Genre, bei Lehrbüchern und Lexika: Das Fachgebiet
- Bei Lehrbüchern: Niveau, bei Lexika: Die Zahl der Artikel
Die Klasse Buchliste
Die Klasse Buchliste übernehmen wir aus der Vorversion, allerdings müssen wir jetzt zusätzlich die für das Datei-Handling notwendigen Java-Klassen importieren.
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path;
Schauen wir uns nun den Quelltext der neuen Methode liesAusDatei(...) an:
public void liesAusDatei(String dateiname)
{
try
{
List zeilen = Files.readAllLines(Path.of(dateiname));
for (String zeile : zeilen)
{
String[] teile = zeile.split(",");
String typ = teile[0];
String autor = teile[1];
String titel = teile[2];
int jahr = Integer.parseInt(teile[3]);
String fachOderGenre = teile[4];
if (typ.equals("Roman"))
{
liste.add(new Roman(titel, autor, fachOderGenre, jahr));
}
else if (typ.equals("Sachbuch"))
{
liste.add(new Sachbuch(titel, autor, fachOderGenre, jahr));
}
else if (typ.equals("Lehrbuch"))
{
Niveau niveau = Niveau.valueOf(teile[5]);
liste.add(new Lehrbuch(titel, autor, fachOderGenre, niveau, jahr));
}
else if (typ.equals("Lexikon"))
{
int artikelzahl = Integer.parseInt(teile[5]);
liste.add(new Lexikon(titel, autor, fachOderGenre, artikelzahl, jahr));
}
}
}
catch (IOException e)
{
System.out.println
("Fehler beim Lesen der Datei: " + e.getMessage());
}
catch (IllegalArgumentException e)
{
System.out.println
("Fehlerhafte Buchdaten: " + a.getMessage());
}
}
Die Erläuterung der Methode liesAusDatei(...)
1. Einlesen der Zeilen
List<String> zeilen = Files.readAllLines(Path.of(dateiname));
Die Methode erhält den Namen der Datei als Parameter, zum Beispiel "buecher.txt". Mit Path.of(dateiname) wird daraus ein Dateipfad erzeugt.
Anschließend liest Files.readAllLines(...) die gesamte Datei ein. Jede Zeile der Datei wird als String gespeichert. Das List-Objekt zeilen enthält also danach alle Zeilen der Datei als Elemente.
2. Zerlegen der Zeilen
for (String zeile : zeilen)
{
String[] teile = zeile.split(",");
Diese for-each-Schleife ist dafür zuständig, die Informationen für die Attributwerte eines Buchs aus dem jeweils nächsten Listenelement (einer Zeile) zu extrahieren. Dazu wird die String-Methode split(...) eingesetzt.
Die Methode split(",") zerlegt die Zeile in ihre Bestandteile, wobei die Kommas als Trennzeichen dienen. Die so gewonnenen Bestandteile - ebenfalls Strings - werden dann in dem Array teile[] gespeichert.
Betrachten wir als Beispiel die Zeile
Roman,Karl May,Winnetou 1,1890,Abenteuerroman
Die split(",")-Methode erzeugt aus dieser Zeile das folgende String-Array:
teile[0] = "Roman" teile[1] = "Karl May" teile[2] = "Winnetou I" teile[3] = "1890" teile[4] = "Abenteuerroman"
Die Elemente dieses Arrays werden nun in lokale Hilfsvariablen übertragen, damit der Quelltext besser lesbar wird:
String typ = teile[0]; String autor = teile[1]; String titel = teile[2]; int jahr = Integer.parseInt(teile[3]); String fachOderGenre = teile[4];
Das Erscheinungsjahr liegt als String vor und muss daher mit der parseInt(...)-Methode der Klasse Integer in eine int-Zahl umgewandelt werden.
Bei einem Sachbuch-Objekt muss noch das Niveau extrahiert werden, bei einem Lexikon-Objekt die Artikelzahl. Beide Angaben finden sich im Arrayelement teile[5].
3. Erzeugung der Buch-Objekte
In den nächsten Zeilen werden die konkreten Objekte der Klassen Roman, Sachbuch, Lehrbuch oder Lexikon erzeugt. Welches Objekt genau erzeugt wird, hängt von der in der Hilfsvariable typ gespeicherten Information ab.
Bei den Klassen Roman und Sachbuch ist die Sache noch einfach:
if (typ.equals("Roman"))
{
liste.add(new Roman(titel, autor, fachOderGenre, jahr));
}
else if (typ.equals("Sachbuch"))
{
liste.add(new Sachbuch(titel, autor, fachOderGenre, jahr));
}
Bei einem Lehrbuch gibt es zusätzlich ein Niveau. Dieses steht in teile[5]. Da Niveau ein Enum ist, wird der Text mit Niveau.valueOf(...) in einen passenden Enum-Wert umgewandelt:
else if (typ.equals("Lehrbuch"))
{
Niveau niveau = Niveau.valueOf(teile[5]);
liste.add(new Lehrbuch(titel, autor, fachOderGenre, niveau, jahr));
}
Bei einem Lexikon steht in der letzten Spalte die Artikelzahl. Auch diese Angabe liegt zunächst als Text vor und wird mit Integer.parseInt(...) in eine ganze Zahl umgewandelt:
else if (typ.equals("Lexikon"))
{
int artikelzahl = Integer.parseInt(teile[5]);
liste.add(new Lexikon(titel, autor, fachOderGenre, artikelzahl, jahr));
}
4. Das Exception-Handling
Wie bereits erwähnt, können beim Lesen von Daten aus einer Datei sehr leicht Fehler auftreten. Daher müssen alle Datei-Operationen zwingend in einem try-Block ausgeführt werden, und die Exceptions müssen in einem catch-Block behandelt werden.
Schauen wir uns den ersten catch-Block an:
catch (IOException e)
{
System.out.println("Fehler beim Lesen der Datei: " + e.getMessage());
}
Der obige catch-Block fängt Fehler ab, die beim Lesen der Datei auftreten können.
Der folgende catch-Block dagegen fängt Fehler ab, die nach dem erfolgreichen Lesen der Datei entstanden sind. So könnte zum Beispiel das eingelesene Jahr keine Zahl sein, oder bei einem Lehrbuch wird ein ungültiges Niveau erkannt:
catch (IllegalArgumentException a)
{
System.out.println("Fehlerhafte Buchdaten: " + a.getMessage());
}
Vertiefung: Die IOException
Wir hatten das Thema Exception-Handling bereits in Folge 8 besprochen. Dort sind wir aber nur auf die unchecked Exceptions eingegangen, also auf Laufzeitfehler, die nicht zwingend behandelt werden müssen. Die IOException gehört dagegen zur Gruppe der checked Exceptions. Diese Ausnahmen müssen entweder behandelt oder an den Aufrufer weitergegeben werden - der Compiler verlangt dies.
Die Methode Files.readAllLines(...) kann eine solche IOException auslösen. Deshalb könnte man beispielsweise den folgenden Quelltext nicht kompilieren:
public void liesDatei(String dateiname)
{
List zeilen =
Files.readAllLines(Path.of(dateiname));
}
Diese einfache Methode enthält keinen try-catch-Block, kann also eine mögliche IOException nicht behandeln. Hier würde der Compiler dann sofort einen Fehler melden:
"Nicht gemeldete Ausnahme java.io.IOException. Muss abgefangen oder deklariert werden, um ausgelöst zu werden"
Es gibt nun zwei Möglichkeiten, wie mit dieser Ausnahme umgegangen werden kann.
- Die Ausnahme wird innerhalb der Methode mit einem try-catch-Block behandelt.
- Die Methode gibt die Ausnahme mit throws IOException an den Aufrufer weiter.
Die erste Variante sähe für dieses einfache Beispiel so aus:
try
{
List zeilen = Files.readAllLines(Path.of(dateiname));
}
catch (IOException e)
{
System.out.println("Dateifehler");
}
Die zweite Variante sähe dann so aus:
public void liesDatei(String dateiname) throws IOException
{
List zeilen =
Files.readAllLines(Path.of(dateiname));
}
In unserem Buch-Projekt verwenden wir die erste Variante. Dadurch kann die Methode selbst auf Fehler reagieren und eine verständliche Fehlermeldung ausgeben, ohne dass das Programm sofort abbricht.
Die zweite Variante (throws ... ) kann man verwenden, wenn der Aufrufer der Methode selbst entscheiden möchte, wie auf die Ausnahme reagiert werden soll.
Wenn das Buch-Projekt beispielsweise keine Konsolen-Ausgaben mehr macht, sondern in einer graphischen Benutzeroberfläche läuft, wäre eine Konsolenmeldung wenig hilfreich. Stattdessen könnte die Anwendung ein Dialogfenster öffnen und den Benutzer über den Fehler informieren. In diesem Fall kann die Methode liesDatei() die Ausnahme mit throws an den Aufrufer weitergeben. Die Benutzeroberfläche entscheidet dann selbst, wie der Fehler dargestellt werden soll.
Darüber hinaus führt die Verwendung von throws häufig zu einer besseren Trennung der Verantwortlichkeiten (Single Responsibility Principle). Die Klasse Buchliste wäre dann ausschließlich für das Einlesen und Verwalten von Büchern zuständig. Die Entscheidung, wie auf Fehler reagiert wird, läge dagegen bei der aufrufenden Klasse.
12.1.4 Schreiben in eine Textdatei
In unserem Buchprojekt hatten wir bisher eine Methode erzeuge(...), mit der wir neue Buch-Objekte anlegen und mit add(...) in die ArrayList<Buch> aufnehmen können:
public void erzeuge()
{
try
{
liste.add(new Sachbuch("Einführung in Java","Meier, Otto","Informatik", 2025));
liste.add(new Sachbuch("Lexikon der Informatik","Duden-Verlag","Informatik",2012));
liste.add(new Roman("Der Herr der Ringe","J.R. Tolkien","Fantasy", 1964));
liste.add(new Lexikon("Römpps Chemielexikon","Römpp, Justus","Chemie",7500,1989));
}
catch (IllegalArgumentException e)
{
System.out.println("Fehler bei liste.erzeuge(): " + e.getMessage());
}
}
In der nächsten Version der Klasse Buchliste wollen wir Buchdaten aus einer Datei in die ArrayList einlesen und um weitere Bücher über die Methode erzeuge(...) ergänzen können. Anschließend soll die erweiterte ArrayList in einer neuen Datei "backup.txt" gespeichert werden. Dazu gehen wir in zwei Teilschritten vor.
1. Erzeugen eines komma-separierten Strings
Jedes Objekt der ArrayList wird in einen komma-separierten String umgewandelt.
private String toDateiString(Buch buch)
{
if (buch instanceof Roman roman)
{
return "Roman,"
+ roman.getAutor() + "," + roman.getTitel() + ","
+ roman.getJahr() + "," + roman.getGenre();
}
// ... Sachbuch und Lehrbuch werden analog behandelt ...
if (buch instanceof Lexikon lexikon)
{
return "Lexikon,"
+ lexikon.getAutor() + "," + lexikon.getTitel() + ","
+ lexikon.getJahr() + "," + lexikon.getFachgebiet() + ","
+ lexikon.getArtikelzahl();
}
return ""; // für alle anderen Fälle ...
}
2. Speichern der komma-separierten Strings in einer Textdatei
public void speichereInDatei(String dateiname)
{
try
{
ArrayList zeilen = new ArrayList<>();
for (Buch buch : liste)
zeilen.add(toDateiString(buch));
Files.write(Path.of(dateiname), zeilen);
System.out.println(zeilen.size() + " Bücher gespeichert.");
}
catch (IOException e)
{
System.out.println("Fehler beim Speichern: " + e.getMessage());
}
}
Zunächst wird in diesem Quelltext eine neue ArrayList<String> namens zeilen erzeugt:
ArrayList<String> zeilen = new ArrayList<>();
Dann werden die Einträge der ArrayList<Buch> nacheinander in komma-separierte Zeichenketten konvertiert und diese dann in der neuen ArrayList<String> gespeichert:
for (Buch buch : liste)
zeilen.add(.toDateiString(buch));
Mit der Anweisung
Files.write(Path.of(dateiname), zeilen);
werden dann alle Elemente der ArrayList zeilen in der angegebenen Textdatei gespeichert. Anschließend wird die Größe von zeilen ausgelesen und in der Konsole angezeigt.
Sollte beim Speichern ein Fehler auftreten, wird dies im catch-Block in der Konsole gemeldet.