Zum Inhalt
wannacode

Dateien

Kurz gesagt: Eine Datei behält die Daten auf der Festplatte, auch nachdem das Programm endet; in Java lesen und schreiben Path und Files Text, und try-with-resources schließt die Datei für dich.

Problem

Das Programm vergisst

Du schreibst ein Programm für deine Ausgaben. Du gibst sie abends ein und schließt es, und am nächsten Tag weiß es nichts mehr von gestern. Hier ist das kürzeste Beispiel:

int visits = 0;
visits++;
System.out.println("Gestartet: " + visits + " Mal.");
Ausgabe
Gestartet: 1 Mal.

Egal wie oft du es startest, es sagt „1 Mal“. Variablen leben im Speicher, solange das Programm läuft. Wenn es endet, wird der Speicher freigegeben, und alles verschwindet.

Damit es sich erinnert, muss das Programm die Daten dort ablegen, wo sie auch nach seinem Ende bleiben: in einer Datei auf der Festplatte.

Die Idee

Pfad und Datei

Hier ist ein ganzes Programm, das eine Notiz in eine Datei schreibt und sie wieder liest:

Main.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) {
Path notes = Path.of("notes.txt");
try {
Files.writeString(notes, "Milch kaufen\nStromrechnung bis zum 15. bezahlen\n");
String text = Files.readString(notes);
System.out.print(text);
} catch (IOException e) {
System.out.println("Problem mit der Datei: " + e.getMessage());
}
}
}
Ausgabe
Milch kaufen
Stromrechnung bis zum 15. bezahlen
  • Path ist der Pfad zur Datei: „wo sie ist“. Path.of("notes.txt") ist eine Datei mit diesem Namen in dem Ordner, aus dem das Programm gestartet wurde.
  • Files hat statische Methoden für alles rund um Dateien. writeString schreibt Text (und legt die Datei an, wenn es sie nicht gibt), readString liest sie ganz.
  • IOException ist eine geprüfte Exception (checked), aus dem Modul „Fehler und Exceptions“. Die Festplatte kann voll sein, die Datei gesperrt oder nicht da, und der Compiler zwingt dich zu sagen, was dann passieren soll.

Starte das Programm und öffne dann den Ordner: notes.txt ist da, und du kannst sie mit jedem Texteditor öffnen. Der nächste Start findet sie.

In den Beispielen ab hier werden die imports nicht gezeigt. Alles ist aus java.nio.file und java.io, und List ist aus java.util.

Die Idee

Zeilen anhängen und lesen

Ausgaben kommen eine nach der anderen dazu. StandardOpenOption.APPEND hängt am Ende an, statt die Datei neu zu schreiben. readAllLines liest die Datei als Zeilen:

Path expenses = Path.of("expenses.txt");
try {
Files.writeString(expenses, "Mittagessen;12.50\n");
Files.writeString(expenses, "Taxi;8.20\n", StandardOpenOption.APPEND);
Files.writeString(expenses, "Buch;18.90\n", StandardOpenOption.APPEND);
List<String> lines = Files.readAllLines(expenses);
double total = 0;
for (String line : lines) {
String[] parts = line.split(";");
total += Double.parseDouble(parts[1]);
System.out.println(parts[0] + ": " + parts[1] + " €");
}
System.out.printf("%d Ausgaben, gesamt %.2f €%n", lines.size(), total);
} catch (IOException e) {
System.out.println("Problem mit der Datei: " + e.getMessage());
}
Ausgabe
Mittagessen: 12.50 €
Taxi: 8.20 €
Buch: 18.90 €
3 Ausgaben, gesamt 39.60 €

readAllLines gibt eine List<String> zurück: eine Liste von Strings, einer für jede Zeile. Listen kommen im nächsten Modul, „Collections“. Für jetzt brauchst du nur zwei Dinge: sie mit for-each durchlaufen, wie ein Array, und size() für die Anzahl der Zeilen.

Genauer, für Neugierige
  • APPEND allein legt die Datei nicht an. Wenn es sie nicht gibt, bekommst du NoSuchFileException. Für „anlegen oder anhängen“ gibst du beides an: StandardOpenOption.CREATE, StandardOpenOption.APPEND.
  • Seit Java 18 ist Text in Dateien standardmäßig in UTF-8, und Umlaute wie „ä“ und das „€“ werden richtig gelesen und geschrieben. In älteren Versionen hing es von den Einstellungen des Computers ab. Wenn du eine Datei von einem anderen System liest, kannst du die Kodierung ausdrücklich angeben: Files.readString(path, StandardCharsets.UTF_8).
  • readAllLines und readString lesen die ganze Datei in den Speicher. Bei einer Datei von mehreren Gigabyte liest du besser Zeile für Zeile. Siehe die Karte über BufferedReader.

Probier es aus

Zwei Schichten

Was gibt das Programm aus?

Path shift = Path.of("shift.txt");
try {
Files.writeString(shift, "Erste Schicht: Jonas\n");
Files.writeString(shift, "Zweite Schicht: Maria\n");
System.out.print(Files.readString(shift));
} catch (IOException e) {
System.out.println(e.getMessage());
}
Antwort

Die Idee

Wenn die Datei nicht da ist

Die Einstellungen des Programms stehen in config.txt, und jemand hat sie gelöscht. Wenn niemand die Exception fängt:

Main.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
static String readConfig() throws IOException {
return Files.readString(Path.of("config.txt"));
}
public static void main(String[] args) throws IOException {
System.out.println(readConfig());
}
}
Fehler
Exception in thread "main" java.nio.file.NoSuchFileException: config.txt
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
...
at java.base/java.nio.file.Files.readString(Files.java:3318)
at Main.readConfig(Main.java:7)
at Main.main(Main.java:11)

Die Zeilen aus java.base sind das Innere von Java (hier mit „...“ gekürzt). Dein Code steht ganz unten: Zeile 7 in readConfig, aufgerufen aus Zeile 11 in main. Auch main hat throws IOException angegeben, deshalb erreicht die Exception die JVM, und das Programm hält an.

NoSuchFileException ist eine Unterklasse von IOException. Fang sie getrennt, vor dem allgemeinen Fall:

static String readConfig() throws IOException {
return Files.readString(Path.of("config.txt"));
}
try {
System.out.println(readConfig());
} catch (NoSuchFileException e) {
System.out.println("Keine Datei " + e.getMessage() + ". Ich nehme die Standardeinstellungen.");
} catch (IOException e) {
System.out.println("Kann die Einstellungen nicht lesen: " + e.getMessage());
}
System.out.println(Files.exists(Path.of("config.txt")));
Ausgabe
Keine Datei config.txt. Ich nehme die Standardeinstellungen.
false

Files.exists(path) prüft, ob die Datei existiert. Die Prüfung ersetzt das catch nicht: Die Datei kann zwischen der Prüfung und dem Lesen verschwinden, und es gibt noch andere Gründe, warum das Lesen scheitern kann.

Die Idee

Zeile für Zeile und try-with-resources

Für eine große Datei oder zum Schreiben in Teilen gibt es BufferedWriter und BufferedReader. Sie halten die Datei offen, solange du mit ihr arbeitest, und müssen deshalb am Ende geschlossen werden:

Path report = Path.of("report.txt");
try (BufferedWriter out = Files.newBufferedWriter(report)) {
for (int day = 1; day <= 3; day++) {
out.write("Tag " + day + ": " + day * 120 + " €");
out.newLine();
}
} catch (IOException e) {
System.out.println("Kann nicht schreiben: " + e.getMessage());
}
try (BufferedReader in = Files.newBufferedReader(report)) {
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Kann nicht lesen: " + e.getMessage());
}
Ausgabe
Tag 1: 120 €
Tag 2: 240 €
Tag 3: 360 €
  • write schreibt Text, newLine() geht in eine neue Zeile.
  • readLine() gibt die nächste Zeile zurück oder null, wenn die Datei zu Ende ist.
  • try (...) mit einer Ressource, die in den Klammern erzeugt wird, heißt try-with-resources. Wenn der Block endet, egal auf welche Weise, schließt sich die Ressource von selbst.

Das Schließen ist keine Formalität: Solange der BufferedWriter offen ist, kann der Text noch in seinem Puffer stehen und nicht auf der Festplatte. Damit der Code kurz bleibt, hat main hier throws IOException:

Path report = Path.of("report.txt");
try (BufferedWriter out = Files.newBufferedWriter(report)) {
for (int day = 1; day <= 3; day++) {
out.write("Tag " + day + ": " + day * 120 + " €");
out.newLine();
}
System.out.println("Im Block: " + Files.size(report) + " Bytes");
}
System.out.println("Nach dem Block: " + Files.size(report) + " Bytes");
for (String line : Files.readAllLines(report)) {
System.out.println(line);
}
Schritt für Schritt, in Worten
  1. Path ist nur die Adresse der Datei. Links ist der Speicher des Programms, rechts die Festplatte.
  2. newBufferedWriter legt die leere Datei report.txt an und ein Objekt mit einem Puffer im Speicher.
  3. write und newLine schreiben in den Puffer. Auf der Festplatte kommt noch nichts an.
  4. Die Datei auf der Festplatte hat noch 0 Bytes.
  5. Der Block endet, und try ruft close() auf. Der Puffer geht auf die Festplatte: Die Datei hat jetzt 45 Bytes.
  6. 45 Bytes für 39 Zeichen mit den Zeilenumbrüchen: In UTF-8 ist jeder lateinische Buchstabe 1 Byte, und € sind 3.
  7. readAllLines liest die drei Zeilen von der Festplatte zurück.

Das haben wir in der Lektion über finally versprochen. Hier ist dasselbe Schreiben mit finally:

BufferedWriter out = null;
try {
out = Files.newBufferedWriter(report);
out.write("Tag 1: 120 €");
} catch (IOException e) {
System.out.println("Kann nicht schreiben: " + e.getMessage());
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
System.out.println("Kann nicht schließen: " + e.getMessage());
}
}
}

Fünfzehn Zeilen für ein einziges Schreiben, und auch close() kann eine IOException werfen. try (...) erledigt genau das für dich.

Genauer, für Neugierige
  • In den Klammern können mehrere Ressourcen stehen, getrennt durch ;. Sie werden in umgekehrter Reihenfolge geschlossen: Die zuletzt geöffnete wird zuerst geschlossen.
  • Puffer. BufferedWriter sammelt den Text im Speicher und schickt ihn in großen Stücken auf die Festplatte. Jeder einzelne Schreibvorgang auf die Festplatte ist langsam, und so gibt es nur wenige davon. Genauso liest BufferedReader viel auf einmal.
  • Bytes und Zeichen. Darunter ist eine Datei eine Folge von Bytes. InputStream und OutputStream (zum Beispiel Files.newInputStream) lesen und schreiben Bytes: für Fotos, Archive, alles, was kein Text ist. Reader und Writer verwandeln Bytes nach der Kodierung in Zeichen. BufferedReader ist ein Reader mit Puffer.
  • Der ältere Weg. In fremdem Code siehst du new BufferedReader(new FileReader("report.txt")) und new Scanner(new File("report.txt")). Sie funktionieren, aber Files ist neuer und bequemer. Scanner liest auch aus einer Datei, mit den hasNextLine und nextLine, die du schon kennst:
try (Scanner in = new Scanner(Path.of("report.txt"))) {
while (in.hasNextLine()) {
System.out.println(in.nextLine());
}
}

Probier es aus

Wann wird geschlossen?

Jede Klasse, die das Interface AutoCloseable implementiert (die Methode close()), kann in einem try (...) stehen. Hier ist der Kassendrucker aus der Lektion über finally, als Ressource:

Was gibt das Programm aus?

class Printer implements AutoCloseable {
Printer() {
System.out.println("Der Drucker ist belegt.");
}
public void close() {
System.out.println("Der Drucker ist frei.");
}
}
try (Printer printer = new Printer()) {
System.out.println("Drucke den Kassenbon...");
int qty = Integer.parseInt("drei");
System.out.println("Menge: " + qty);
} catch (NumberFormatException e) {
System.out.println("Falsche Menge.");
}
Antwort

Probier es aus

Finde den Fehler

Das Programm schreibt eine Bestellung und zählt die Zeilen in der Datei. Es gibt 0 aus. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Ordner

Die Sicherungskopien der Kasse kommen in einen Ordner, eine pro Tag:

Path backup = Path.of("backup");
try {
Files.createDirectories(backup.resolve("old"));
Files.writeString(backup.resolve("2026-10-01.csv"), "Brötchen,0.60\n");
Files.writeString(backup.resolve("2026-10-02.csv"), "Brötchen,0.60\nTee,1.80\n");
String[] names = backup.toFile().list();
Arrays.sort(names);
for (String name : names) {
Path p = backup.resolve(name);
if (Files.isDirectory(p)) {
System.out.println(name + "/");
} else {
System.out.println(name + " (" + Files.size(p) + " Bytes)");
}
}
Files.delete(backup.resolve("2026-10-01.csv"));
System.out.println(Files.exists(backup.resolve("2026-10-01.csv")));
} catch (IOException e) {
System.out.println(e.getMessage());
}
Ausgabe
2026-10-01.csv (15 Bytes)
2026-10-02.csv (24 Bytes)
old/
false
Methode Was sie tut
Files.createDirectories(p) legt den Ordner und alle Ordner darüber an, wenn es sie nicht gibt
dir.resolve("name") der Pfad zu name in dir
Files.isDirectory(p), Files.exists(p) ist es ein Ordner, ist es da
Files.size(p) die Größe in Bytes
Files.delete(p) löscht eine Datei oder einen leeren Ordner
dir.toFile().list() ein Array mit den Namen von allem im Ordner

Die Größen sind 15 und 24 Bytes, obwohl es weniger Zeichen sind: In UTF-8 ist das „ö“ in „Brötchen“ 2 Bytes.

list() gibt die Namen in keiner bestimmten Reihenfolge, deshalb sortieren wir sie mit java.util.Arrays.sort.
Genauer, für Neugierige
  • Files.delete wirft DirectoryNotEmptyException, wenn der Ordner nicht leer ist, und NoSuchFileException, wenn es ihn nicht gibt. Files.deleteIfExists beschwert sich nicht, wenn es ihn nicht gibt.
  • Files.list(dir) ist der neue Weg zum Inhalt eines Ordners, aber es gibt einen Stream zurück, der im Modul „Modernes Java“ kommt. Files.copy und Files.move kopieren und verschieben.
  • In älterem Code siehst du die Klasse java.io.File: new File("notes.txt") mit den Methoden exists(), length(), delete(), renameTo(...), createNewFile() und getAbsolutePath(). Sie gab es vor Path und Files, und path.toFile() und file.toPath() verwandeln das eine in das andere. Dort ist das Anhängen am Ende new FileWriter("output.txt", true).
  • Der Pfad "notes.txt" ist relativ: zu dem Ordner, aus dem das Programm gestartet wurde. Path.of("/home/maria/notes.txt") ist absolut. path.toAbsolutePath() zeigt genau, wo die Datei ist.

Übung

Programme, die sich erinnern

Löse die Aufgaben in deinem Editor. In jeder Aufgabe legt das Programm die Eingabedatei zuerst selbst an, mit Files.writeString, damit du es sofort ausprobieren kannst.

1. Eine Konfigurationsdatei. Lege server.conf mit diesem Inhalt an, lies sie dann und gib jede Einstellung als Schlüssel: Wert aus. Überspringe die leeren Zeilen und die Zeilen, die mit # beginnen. Tipp: split("=", 2) teilt nur am ersten =.

server.conf
# Servereinstellungen
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Ausgabe
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. Ein Filter für eine Logdatei. Lege app.log mit vier Zeilen an (zwei INFO und zwei ERROR). Lies ein Level von der Tastatur, zum Beispiel ERROR, und schreib die Zeilen mit diesem Level in errors.log mit einem BufferedReader und einem BufferedWriter in einem try (...). Gib errors.log aus und wie viele Zeilen du geschrieben hast.

Eingabe
ERROR
Ausgabe
ERROR: Failed to load configuration
ERROR: Null pointer exception
2 Zeilen in errors.log geschrieben

3. Der Inhalt eines Ordners. Lege einen Ordner project mit den Dateien document.txt, image.jpg, script.js und dem Ordner backup an. Schreib ihre Namen, alphabetisch sortiert, in directory_contents.txt, mit einem / nach den Namen von Ordnern. Gib die Datei aus.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. Eine Indexdatei. Lege text.txt mit den drei Zeilen unten an. Lies ein Wort von der Tastatur und schreib in index.txt jede Stelle, an der es vorkommt, in der Form Zeile:Position: die Nummer der Zeile (ab 1) und den Index in der Zeile (ab 0). Wenn das Wort zweimal in einer Zeile steht, sind das zwei Einträge.

text.txt
hello world
world hello
hello again, hello
Eingabe
hello
index.txt
1:0
2:6
3:0
3:13

Weitere Aufgaben

  • Zähler für die Starts. Repariere das Programm aus der ersten Karte: Zähl die Starts in visits.txt. Wenn es die Datei nicht gibt, fang bei 0 an.
  • Suchen und Ersetzen in einer Datei. Die Aufgabe aus der Lektion „Reguläre Ausdrücke: Gruppen und Ersetzen“: Lies eine Datei, ersetze „alt“ durch „neu“ und schreib das Ergebnis in eine neue Datei.
  • Datumsangaben aus einer Datei. Die Datumsaufgabe aus der Lektion „Reguläre Ausdrücke“: Lies die Datumsangaben aus einer Datei, eine pro Zeile.
  • Eine Kopie mit Zeilennummern. Lies eine Datei und schreib eine Kopie, in der jede Zeile mit ihrer Nummer beginnt: 1: ..., 2: ....

Test

Teste dich

1. Warum verlangt der Compiler ein try/catch um Files.readString?
2. Files.writeString(file, "A\n"); danach Files.writeString(file, "B\n"); Was steht in der Datei?
3. Wann ruft try-with-resources close() auf?
4. Was gibt in.readLine() zurück, wenn die Datei zu Ende ist?
5. Du schreibst mit einem BufferedWriter, schließt ihn aber nicht. Was kann in der Datei stehen?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·