1. Einleitung
Die Arbeit mit Dateien in Java (und nicht nur dort) ist immer die Arbeit mit externen Ressourcen. Wenn Sie eine Datei öffnen, weist das Betriebssystem Ihrer Anwendung einen „Dateideskriptor“ – eine spezielle Kennung – zu, mit dem Sie aus der Datei lesen und in sie schreiben können. Die Anzahl solcher Deskriptoren ist begrenzt: Wenn Sie Dateien nicht schließen, kann Ihr Programm schnell alle verfügbaren Ressourcen „verbrauchen“ und beginnt, rätselhafte Fehlermeldungen wie "Too many open files" auszuwerfen.
Mehr noch: Wenn eine Datei nicht geschlossen wird, kann sie für andere Programme gesperrt bleiben. Zum Beispiel: Sie haben eine Datei zum Schreiben geöffnet, sie zu schließen vergessen und nun kann weder Sie noch jemand anders sie ändern oder löschen. Eine Art „ewige Geiselnahme“ in der Welt der Dateisysteme.
Beispiel aus der Praxis
FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... wir machen etwas und vergessen dann fis.close()
Wenn die Methode close() nicht aufgerufen wird, bleibt die Datei „hängend“, bis das Programm beendet ist. In großen Anwendungen kann das zu Ressourcenlecks und sogar zum Absturz der Anwendung führen.
2. Der alte Weg: finally + close()
Bis Java 7 sah die klassische Methode, eine Datei garantiert zu schließen, so aus:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// Datei lesen
int b = fis.read();
// ...
} catch (IOException e) {
System.out.println("Fehler beim Lesen der Datei: " + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("Fehler beim Schließen der Datei: " + e.getMessage());
}
}
}
Nachteile dieses Ansatzes
- Man vergisst leicht den finally-Block und handelt sich ein Ressourcenleck ein.
- Viel unnötiger Code, besonders wenn es mehrere Streams gibt.
- Wenn beim Schließen eine Ausnahme auftritt, muss sie separat abgefangen werden.
- Der Code wird aufgebläht und schlechter lesbar.
3. Moderner Ansatz: try-with-resources
Zum Glück hat Java 7 eine Syntax eingeführt, die diese Probleme elegant und automatisch löst – try-with-resources.
So sieht das aus
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b = fis.read();
// mit der Datei arbeiten
} catch (IOException e) {
System.out.println("Fehler bei der Arbeit mit der Datei: " + e.getMessage());
}
// Hier ist fis bereits automatisch geschlossen!
Der Hauptvorteil: Alle Ressourcen, die in runden Klammern nach try deklariert sind, werden nach Ende des Blocks automatisch geschlossen – selbst wenn in der Mitte eine Ausnahme aufgetreten ist. Kein finally mehr nötig, keine separaten Fehler beim Schließen abfangen – Java erledigt das für Sie.
Welche Klassen können in try-with-resources verwendet werden?
Jede Klasse, die das Interface AutoCloseable (oder den guten alten Closeable) implementiert. Das gilt für fast alle Standard-I/O-Streams: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter, Scanner, PrintWriter und viele andere.
4. Syntax von try-with-resources: Details und Beispiele
Eine Ressource
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Fehler: " + e.getMessage());
}
// reader wird automatisch geschlossen!
Mehrere Ressourcen
Sie können mehrere Ressourcen durch Semikolon getrennt deklarieren:
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
System.out.println("Fehler beim Kopieren: " + e.getMessage());
}
// beide Streams sind geschlossen!
Schließreihenfolge: Ressourcen werden in umgekehrter Reihenfolge ihrer Deklaration geschlossen. Zuerst wird writer.close() aufgerufen, danach reader.close(). Das ist wichtig, wenn ein Stream vom anderen abhängt.
Verwendung mit eigenen Klassen
Wenn Sie eine eigene Klasse schreiben, die mit Ressourcen arbeitet, implementieren Sie einfach das Interface AutoCloseable:
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("Wir arbeiten mit der Ressource!");
}
@Override
public void close() {
System.out.println("Ressource geschlossen!");
}
}
try (MyResource res = new MyResource()) {
res.doSomething();
}
// Nach dem Verlassen des Blocks wird ausgegeben: "Ressource geschlossen!"
5. So funktioniert es: Schema
flowchart TD
A[Öffnen einer Ressource in try-with-resources] --> B{Ist im try-Block eine Ausnahme aufgetreten?}
B -- Nein --> C[Ressource wird automatisch geschlossen]
B -- Ja --> D[Ressource wird automatisch geschlossen]
D --> E[Ausnahme wird weitergegeben]
C --> F[Programm wird fortgesetzt]
Fazit: Unabhängig davon, ob ein Fehler aufgetreten ist oder nicht – die Ressource wird immer geschlossen!
6. Beispiele: Code „auf die neue Art“ umschreiben
Vorher (alter Stil):
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Fehler: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Fehler beim Schließen: " + e.getMessage());
}
}
}
Nachher (try-with-resources):
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Fehler: " + e.getMessage());
}
// Das war’s, kein finally mehr!
7. Was passiert bei einem Fehler während des Schließens?
Manchmal kann der Schließvorgang selbst eine Ausnahme auslösen (zum Beispiel, wenn die Datei plötzlich verschwunden ist). In try-with-resources gehen solche Ausnahmen nicht verloren: Wenn im try-Block bereits eine Ausnahme aufgetreten ist und beim Schließen der Ressource eine zweite, wird diese als „unterdrückt“ (suppressed exception) zur Hauptausnahme hinzugefügt. Das lässt sich mit der Methode Throwable.getSuppressed() anzeigen.
Beispiel
try (MyResource res = new MyResource()) {
throw new IOException("Fehler im try-Block");
} catch (IOException e) {
System.out.println("Hauptfehler: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Unterdrückte Ausnahme: " + suppressed.getMessage());
}
}
8. Welche Klassen unterstützen try-with-resources?
Ganz einfach: jede Klasse, die das Interface AutoCloseable implementiert. Hier nur einige der Standardklassen:
| Klasse | Zweck |
|---|---|
|
Lesen von Bytes aus einer Datei |
|
Schreiben von Bytes in eine Datei |
|
Text lesen/schreiben |
|
Pufferung von Streams |
|
Schreiben von formatiertem Text |
|
Lesen von Daten aus Datei/Konsole |
|
Serialisierung/Deserialisierung |
|
Arbeiten mit ZIP-Archiven |
|
Netzwerkverbindungen |
Wenn Sie Drittanbieterbibliotheken verwenden – sehen Sie in die Dokumentation: Gibt es eine Methode close(), unterstützt die Klasse sehr wahrscheinlich try-with-resources.
9. Tipps und nützliche Feinheiten
Variablen können außerhalb von try deklariert werden (seit Java 9): Bereits deklarierte Ressourcen können verwendet werden, wenn sie final oder „effectively final“ sind:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
// ...
}
Funktioniert nicht nur mit Dateien: try-with-resources ist praktisch für alle Ressourcen: Netzwerkverbindungen, Datenbanken, beliebige Objekte mit einer Methode close().
Ignorieren Sie Ausnahmen nicht: Auch mit try-with-resources sollten Ausnahmen abgefangen und behandelt werden – das ist kein Allheilmittel, sondern „nur“ ein bequemer Weg, Lecks zu vermeiden.
Schließen Sie die Ressource nicht manuell innerhalb von try: Das ist nicht nötig – Java übernimmt das für Sie! Wenn Sie close() manuell aufrufen und der try-Block danach endet, wird versucht, eine bereits geschlossene Ressource erneut zu schließen. Das ist zwar meist harmlos, kann aber verwirren.
10. Typische Fehler bei der Verwendung von try-with-resources
Fehler Nr. 1: try-with-resources gar nicht benutzt. Wenn Sie immer noch finally { resource.close(); } schreiben – dann sind Sie entweder im Jahr 2011 oder haben diese Vorlesung nicht gelesen! Verwenden Sie die moderne Syntax.
Fehler Nr. 2: Ressource außerhalb von try deklariert und innen nur verwendet. Solcher Code schließt die Ressource nicht automatisch:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
// ... reader verwenden
} finally {
// Und hier wurde das Schließen vergessen!
}
Fehler Nr. 3: close() manuell im try-Block aufrufen. Das ist nicht kritisch, aber überflüssig und kann zu doppeltem Schließen führen. Vertrauen Sie einfach Java.
Fehler Nr. 4: Nur Exception abfangen und I/O-Spezifika ignorieren. Fangen Sie besser spezifische Ausnahmen (FileNotFoundException, IOException), um dem Benutzer verständliche Meldungen zu geben.
Fehler Nr. 5: Unterdrückte Ausnahmen nicht berücksichtigen. Wenn beim Schließen der Ressource ein Fehler auftrat, kann er „unterdrückt“ worden sein. Wenn Sie Fehler analysieren, vergessen Sie getSuppressed() nicht.
GO TO FULL VERSION