try-with-resources und AutoCloseable in Java
1. Überblick
Dateien, Sockets, Datenbankverbindungen und Payment-Gateway-Sitzungen sind Ressourcen, die nach der Nutzung geschlossen werden müssen. Wenn Sie vergessen zu schließen, wird Ihre Anwendung File-Deskriptoren verlieren, die Connection Pool erschöpfen oder Locks länger als nötig halten.
Dieser Artikel zeigt Ihnen, wie Sie try-with-resources verwenden, um Ressourcen automatisch zu schließen. Sie werden auch lernen, wie Sie eine Klasse schreiben, die AutoCloseable implementiert, wie Java Exceptions beim Schließen behandelt (suppressed Exceptions), und häufige Fehler bei der Verwendung dieser Syntax.
2. Das Problem mit try-finally
Vor Java 7 war der Standard-Weg zum Schließen von Ressourcen die Verwendung eines finally-Blocks:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class LegacyOrderReader {
public static String readFirstLine(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine();
} finally {
reader.close();
}
}
}
Dieser Code funktioniert, aber es gibt zwei Probleme:
- Umständlich. Wenn Sie zwei oder drei Ressourcen haben, müssen Sie mehrere
try-finally-Blöcke verschachteln. Jeder Block muss auch aufnullüberprüft werden. - Maskiert die ursprüngliche Exception. Wenn
readLine()eineIOExceptionwirft und dann auchclose()eine Exception wirft, ersetzt die Exception aus demfinally-Block die ursprüngliche Exception. Sie verlieren Informationen über den tatsächlichen Fehler.
try-with-resources löst beide Probleme.
3. Die Syntax von try-with-resources
Sie deklarieren eine Ressource in Klammern direkt nach dem try-Schlüsselwort:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class OrderReader {
public static String readFirstLine(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
}
}
Wenn der try-Block endet, ruft Java automatisch reader.close() auf. Dies gilt, ob der Block normal endet, auf return trifft oder eine Exception wirft. Sie müssen kein finally schreiben.
Die einzige Voraussetzung ist, dass der Typ der Variable java.lang.AutoCloseable implementiert. Die meisten I/O-Klassen im JDK erfüllen diese Voraussetzung, wie InputStream, Reader, Connection, Statement, ResultSet und Scanner.
3.1. Mehrere Ressourcen und die Schließreihenfolge
Sie können mehrere Ressourcen deklarieren, getrennt durch Semikola. Java schließt sie in umgekehrter Reihenfolge ihrer Deklaration:
public class ClosingOrderDemo {
record NamedResource(String name) implements AutoCloseable {
NamedResource {
System.out.println("Öffnen " + name);
}
@Override
public void close() {
System.out.println("Schließen " + name);
}
}
public static void main(String[] args) {
try (var connection = new NamedResource("connection");
var statement = new NamedResource("statement");
var resultSet = new NamedResource("resultSet")) {
System.out.println("Kundendaten lesen");
}
}
}
Ausgabe:
Öffnen connection
Öffnen statement
Öffnen resultSet
Kundendaten lesen
Schließen resultSet
Schließen statement
Schließen connection
Diese Reihenfolge ist sinnvoll. Ressourcen, die später deklariert werden, hängen normalerweise von früher deklarierten Ressourcen ab, wie statement hängt von connection ab, daher müssen sie zuerst geschlossen werden.
Es gibt einen wichtigen Punkt zu beachten. Wenn die zweite Ressource sofort im Constructor eine Exception wirft, schließt Java trotzdem die erste Ressource, die erfolgreich geöffnet wurde. Ressourcen, die nicht erstellt wurden, haben close() nicht aufgerufen.
3.2. Eine bereits deklarierte Variable verwenden (Java 9+)
Ab Java 9 können Sie eine außerhalb deklarierte Variable in try einbeziehen, solange die Variable final oder effectively final ist:
import java.util.Scanner;
public class CustomerInput {
public static void main(String[] args) {
Scanner scanner = new Scanner("Nguyen Van A\n0901234567");
try (scanner) {
System.out.println("Name: " + scanner.nextLine());
System.out.println("Telefon: " + scanner.nextLine());
}
}
}
Dies ist praktisch, wenn die Ressource als Parameter übergeben wird oder von einer komplexen Factory erstellt wird.
3.3. catch und finally laufen nach dem Schließen
try-with-resources erlaubt immer noch, catch und finally hinzuzufügen. Jedoch laufen beide nachdem die Ressourcen geschlossen wurden. Daher können Sie in catch die Ressource nicht erneut verwenden, und die Ressourcenvariable ist nicht mehr im Geltungsbereich.
4. Eine eigene AutoCloseable-Klasse schreiben
AutoCloseable hat nur eine Methode:
void close() throws Exception;
Angenommen, Sie haben eine Payment-Gateway-Sitzung, die nach jeder Bestellung freigegeben werden muss:
public class PaymentGatewaySession implements AutoCloseable {
private final String orderId;
private boolean closed;
public PaymentGatewaySession(String orderId) {
this.orderId = orderId;
System.out.println("Payment-Sitzung für " + orderId + " geöffnet");
}
public void charge(long amount) {
if (closed) {
throw new IllegalStateException("Sitzung ist geschlossen");
}
if (amount <= 0) {
throw new IllegalArgumentException("Ungültiger Betrag: " + amount);
}
System.out.println(amount + " VND für Bestellung " + orderId + " abgebucht");
}
public boolean isClosed() {
return closed;
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
System.out.println("Payment-Sitzung für " + orderId + " geschlossen");
}
}
Es gibt drei Punkte in dieser Klasse, die beachtet werden sollten:
close()deklariert nichtthrows Exception. Eine Override-Methode darf die Liste der geprüften Exceptions einengen. Dadurch können die Benutzer Ihrer Klasse nicht gezwungen werden,catch (Exception e)zu fangen.close()ist idempotent: Mehrfaches Aufrufen verursacht keine Fehler und gibt nicht zweimal frei.charge()prüft den Statusclosed, um die Verwendung der Ressource nach dem Schließen zu verhindern.
Verwendung:
public class CheckoutService {
public static void main(String[] args) {
try (var session = new PaymentGatewaySession("ORD-1001")) {
session.charge(250_000);
}
}
}
Payment-Sitzung für ORD-1001 geöffnet
250000 VND für Bestellung ORD-1001 abgebucht
Payment-Sitzung für ORD-1001 geschlossen
4.1. AutoCloseable oder Closeable?
java.io.Closeable existiert seit Java 5 und erbt von AutoCloseable. Diese zwei Interfaces unterscheiden sich in folgenden Punkten:
AutoCloseable |
Closeable |
|
|---|---|---|
Exception von close() |
Exception |
IOException |
| Idempotent | Empfohlen | Erforderlich nach Vertrag |
| Zweck | Jede Art von Ressource | I/O-Ressourcen |
Wenn Ihre Ressource I/O betrifft, verwenden Sie Closeable. Für andere Fälle, wie Locks, Sitzungen oder Transaktionen, verwenden Sie AutoCloseable und engen Sie throws so viel wie möglich ein.
5. Suppressed Exceptions
Zurück zum Problem aus Punkt 2: Der try-Block wirft eine Exception, dann wirft auch close() eine Exception. Bei try-with-resources wird die Exception aus dem try-Block als Haupt-Exception beibehalten. Die Exception von close() wird als unterdrückt an die Haupt-Exception angehängt:
public class SuppressedDemo {
static class FlakyConnection implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("Fehler beim Schließen der Verbindung");
}
}
public static void main(String[] args) {
try (var connection = new FlakyConnection()) {
throw new RuntimeException("Fehler beim Speichern der Bestellung");
} catch (RuntimeException e) {
System.out.println("Hauptfehler: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Unterdrückt: " + suppressed.getMessage());
}
}
}
}
Ausgabe:
Hauptfehler: Fehler beim Speichern der Bestellung
Unterdrückt: Fehler beim Schließen der Verbindung
Sie behalten beide Informationen. Wenn Sie den Stack-Trace mit e.printStackTrace() oder über einen Logger ausgeben, wird auch der Suppressed:-Teil angezeigt. Dies macht das Debugging in der Produktion viel einfacher.
Wenn der try-Block normal ausgeführt wird und nur close() eine Exception wirft, wird die Exception von close() die Haupt-Exception.
6. Tests für das Schließen von Ressourcen
Um Tests zu schreiben, müssen Sie JUnit 5 und AssertJ hinzufügen:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
Der folgende Test überprüft zwei Hauptverhaltensweisen: Die Ressource wird immer geschlossen, auch wenn ein Fehler auftritt, und die Exception von close() wird als unterdrückt an die Haupt-Exception angehängt.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class PaymentGatewaySessionTest {
@Test
void shouldCloseSessionEvenWhenChargeFails() {
var session = new PaymentGatewaySession("ORD-2002");
assertThatThrownBy(() -> {
try (session) {
session.charge(-5);
}
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("-5");
assertThat(session.isClosed()).isTrue();
}
@Test
void shouldAttachCloseFailureAsSuppressed() {
AutoCloseable failingResource = () -> {
throw new IllegalStateException("close failed");
};
assertThatThrownBy(() -> {
try (failingResource) {
throw new RuntimeException("business failed");
}
}).hasMessage("business failed")
.satisfies(e -> assertThat(e.getSuppressed())
.singleElement()
.extracting(Throwable::getMessage)
.isEqualTo("close failed"));
}
@Test
void closeShouldBeIdempotent() {
var session = new PaymentGatewaySession("ORD-3003");
session.close();
session.close();
assertThat(session.isClosed()).isTrue();
assertThatThrownBy(() -> session.charge(100))
.isInstanceOf(IllegalStateException.class);
}
}
Der zweite Test verwendet einen Lambda, um schnell ein AutoCloseable zu erstellen. Dies funktioniert, weil AutoCloseable nur eine abstrakte Methode hat und als funktionales Interface verwendet werden kann.
7. Häufige Fehler
7.1. Ressourcen im Constructor verschachteln
try (var in = new ObjectInputStream(new FileInputStream("orders.bin"))) {
// ...
}
Der Constructor von ObjectInputStream liest den Header des Streams und kann eine IOException werfen. In diesem Fall wurde die Variable in nicht zugewiesen. FileInputStream im Inneren wurde geöffnet, wird aber niemals geschlossen. Der sichere Weg ist, jede Ressource separat zu deklarieren:
try (var file = new FileInputStream("orders.bin");
var in = new ObjectInputStream(file)) {
// ...
}
7.2. Ressource aus dem try-Block zurückgeben
public static Stream<String> loadOrderLines(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path)) {
return lines; // Falsch: Stream ist bereits geschlossen
}
}
Der Aufrufer erhält sofort eine IllegalStateException, wenn er den Stream verwendet. Sie haben zwei Möglichkeiten, dies zu beheben. Der erste Weg ist, alle Daten innerhalb von try zu verarbeiten, z.B. return lines.toList();. Der zweite Weg ist, den Stream zurückzugeben und den Aufrufer lassen, ihn selbst in try-with-resources zu wickeln.
Hinweis: Nur
Stream, die an I/O-Ressourcen gebunden sind, müssen geschlossen werden, wieFiles.lines,Files.listoderFiles.walk. Stream, die vonList.stream()erstellt werden, müssen nicht geschlossen werden.
7.3. InterruptedException in close() werfen
Das Javadoc von AutoCloseable empfiehlt, keine InterruptedException von close() zu werfen. Der Grund ist, dass diese Exception unterdrückt werden kann, wodurch der Interrupt-Status des Threads verloren geht. Wenn Sie in close() warten müssen, fangen Sie die Exception und rufen Thread.currentThread().interrupt() auf.
7.4. Null-Ressourcen
Die Deklaration try (var conn = findConnectionOrNull()) ist legal. Wenn der Wert null ist, überspringt Java einfach den Aufruf von close(). Dies ist jedoch normalerweise ein Zeichen schlechten Designs. Sie sollten Optional verwenden oder eine Exception von der Factory werfen.
7.5. Wann sollte man es nicht verwenden
Nicht jedes Objekt, das AutoCloseable implementiert, sollte sofort in einer Methode geschlossen werden. In Spring Boot beispielsweise sind gemeinsam genutzte DataSource oder HttpClient Beans, die zusammen mit dem Application Context leben. Spring wird sie schließen, wenn der Context herunterfährt. Wenn Sie einen gemeinsam genutzten Bean in try-with-resources wickeln, schließen Sie ihn, während andere Komponenten ihn noch verwenden.
Verwenden Sie try-with-resources nur für Ressourcen, die Ihr Code erstellt und besitzt in diesem Bereich.
7.6. Tipp: ExecutorService in Java 21
Ab Java 19 implementiert ExecutorService AutoCloseable. Die Methode close() wartet auf die Fertigstellung aller Tasks. Dies ist sehr praktisch bei der Verwendung von virtuellen Threads:
import java.util.concurrent.Executors;
public class InvoiceBatch {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 1; i <= 3; i++) {
int invoiceId = i;
executor.submit(() -> System.out.println("Rechnung #" + invoiceId + " senden"));
}
}
System.out.println("Alle Rechnungen versendet");
}
}
Die Zeile “Alle Rechnungen versendet” wird immer zuletzt gedruckt, da close() blockiert, bis alle drei Tasks beendet sind. Die Reihenfolge der drei Rechnungen ist nicht festgelegt.
8. Fazit
try-with-resources macht Code kürzer und sicherer als try-finally. Ressourcen werden immer geschlossen, in umgekehrter Reihenfolge ihrer Öffnung. Die ursprüngliche Exception wird nicht maskiert, dank des Mechanismus der unterdrückten Exceptions.
Wenn Sie eine AutoCloseable-Klasse schreiben, sollten Sie:
- das
throwsvonclose()einengen; close()idempotent machen;- die Verwendung des Objekts nach dem Schließen verhindern.
Vermeiden Sie darüber hinaus, Ressourcen im Constructor zu verschachteln, geben Sie Ressourcen nicht aus dem try-Block zurück, und schließen Sie nur Ressourcen, die Ihr Code wirklich besitzt.