try-with-resources et AutoCloseable en Java
1. Aperçu
Les fichiers, sockets, connexions de base de données et les sessions de travail avec une passerelle de paiement sont tous des ressources qui doivent être fermées après utilisation. Si vous oubliez de les fermer, votre application aura des fuites de descripteurs de fichiers, épuisera le pool de connexions ou gardera des locks plus longtemps que nécessaire.
Cet article explique comment utiliser try-with-resources pour fermer automatiquement les ressources. Vous apprendrez aussi comment écrire votre propre classe implémentant AutoCloseable, comment Java gère les exceptions levées lors de la fermeture (suppressed exception), et les erreurs courantes avec cette syntaxe.
2. Le problème avec try-finally
Avant Java 7, la façon standard de fermer une ressource était d’utiliser un bloc finally :
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();
}
}
}
Ce code fonctionne, mais il y a deux problèmes :
- Verbeux. Avec deux ou trois ressources, vous devez imbriquer plusieurs blocs
try-finally. Chaque bloc nécessite aussi une vérificationnull. - Masque l’exception originale. Si
readLine()lève uneIOException, puis queclose()lève aussi une exception, l’exception du blocfinallyremplacera l’exception originale. Vous perdez l’information sur l’erreur réelle.
try-with-resources résout ces deux problèmes.
3. Syntaxe try-with-resources
Vous déclarez la ressource entre des parenthèses juste après le mot-clé try :
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();
}
}
}
Quand le bloc try se termine, Java appelle automatiquement reader.close(). Cela s’applique aussi bien quand le bloc s’exécute normalement, quand il y a un return, et quand il y a une exception. Vous n’avez pas besoin d’écrire finally.
La seule condition est que le type de la variable doit implémenter java.lang.AutoCloseable. La plupart des classes I/O dans la JDK satisfont cette condition, par exemple InputStream, Reader, Connection, Statement, ResultSet et Scanner.
3.1. Plusieurs ressources et ordre de fermeture
Vous pouvez déclarer plusieurs ressources, séparées par des points-virgules. Java les ferme dans l’ordre inverse de leur déclaration :
public class ClosingOrderDemo {
record NamedResource(String name) implements AutoCloseable {
NamedResource {
System.out.println("Ouvre " + name);
}
@Override
public void close() {
System.out.println("Ferme " + 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("Lire les données client");
}
}
}
Résultat :
Ouvre connection
Ouvre statement
Ouvre resultSet
Lire les données client
Ferme resultSet
Ferme statement
Ferme connection
Cet ordre est logique. La ressource déclarée plus tard dépend généralement de la ressource déclarée avant, par exemple statement dépend de connection, elle doit donc être fermée en premier.
Il y a un point à retenir. Si la deuxième ressource lève une exception dans son constructeur, Java ferme quand même la première ressource qui a été ouverte avec succès. Les ressources qui n’ont pas encore été créées ne seront pas fermées.
3.2. Utiliser une variable existante (Java 9+)
À partir de Java 9, vous pouvez utiliser dans try une variable déclarée à l’extérieur, pourvu qu’elle soit final ou effectivement finale :
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("Nom : " + scanner.nextLine());
System.out.println("Téléphone : " + scanner.nextLine());
}
}
}
Cette approche est pratique quand la ressource est passée en paramètre ou créée par une factory complexe.
3.3. catch et finally s’exécutent après la fermeture
try-with-resources permet toujours d’ajouter des blocs catch et finally. Cependant, les deux s’exécutent après que la ressource ait été fermée. Par conséquent, dans catch vous ne pouvez pas réutiliser cette ressource, et la variable de la ressource n’est plus en scope.
4. Écrire sa propre classe AutoCloseable
AutoCloseable n’a qu’une seule méthode :
void close() throws Exception;
Supposons que vous ayez une session de travail avec une passerelle de paiement, et que cette session doit être libérée après chaque commande :
public class PaymentGatewaySession implements AutoCloseable {
private final String orderId;
private boolean closed;
public PaymentGatewaySession(String orderId) {
this.orderId = orderId;
System.out.println("Ouvre la session de paiement pour " + orderId);
}
public void charge(long amount) {
if (closed) {
throw new IllegalStateException("La session est fermée");
}
if (amount <= 0) {
throw new IllegalArgumentException("Montant invalide : " + amount);
}
System.out.println("Débiter " + amount + " VND pour la commande " + orderId);
}
public boolean isClosed() {
return closed;
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
System.out.println("Ferme la session de paiement pour " + orderId);
}
}
Il y a trois points à noter dans cette classe :
close()ne déclare pasthrows Exception. Une méthode override peut restreindre la liste des checked exception. Grâce à cela, les utilisateurs de votre classe ne seront pas forcés de fairecatch (Exception e).close()idempotent : l’appeler plusieurs fois ne provoque pas d’erreur et ne libère pas deux fois.charge()vérifie l’étatclosedpour empêcher l’utilisation de la ressource après fermeture.
Utilisation :
public class CheckoutService {
public static void main(String[] args) {
try (var session = new PaymentGatewaySession("ORD-1001")) {
session.charge(250_000);
}
}
}
Ouvre la session de paiement pour ORD-1001
Débiter 250000 VND pour la commande ORD-1001
Ferme la session de paiement pour ORD-1001
4.1. AutoCloseable ou Closeable ?
java.io.Closeable existe depuis Java 5 et étend AutoCloseable. Ces deux interfaces diffèrent sur les points suivants :
AutoCloseable |
Closeable |
|
|---|---|---|
Exception de close() |
Exception |
IOException |
| Idempotent | Recommandé | Obligatoire par contrat |
| Objectif | Tout type de ressource | Ressources I/O |
Si votre ressource concerne les I/O, utilisez Closeable. Pour les autres cas, comme les locks, les sessions de travail ou les transactions, utilisez AutoCloseable et restreignez les throws autant que possible.
5. Suppressed exception
Revenons au problème du point 2 : le bloc try lève une exception, puis close() lève aussi une exception. Avec try-with-resources, l’exception du bloc try est conservée comme exception principale. L’exception de close() est attachée à l’exception principale sous forme de suppressed :
public class SuppressedDemo {
static class FlakyConnection implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("Erreur lors de la fermeture de la connexion");
}
}
public static void main(String[] args) {
try (var connection = new FlakyConnection()) {
throw new RuntimeException("Erreur lors de l'enregistrement de la commande");
} catch (RuntimeException e) {
System.out.println("Principal : " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Supprimé : " + suppressed.getMessage());
}
}
}
}
Résultat :
Principal : Erreur lors de l'enregistrement de la commande
Supprimé : Erreur lors de la fermeture de la connexion
Vous conservez les deux informations. Quand vous affichez la stack trace avec e.printStackTrace() ou via un logger, la partie Suppressed: apparaît aussi. Grâce à cela, le débogage en production est beaucoup plus facile.
Si le bloc try s’exécute normalement et que seul close() lève une exception, alors l’exception de close() sera l’exception principale.
6. Tester le comportement de fermeture des ressources
Pour écrire des tests, vous devez ajouter JUnit 5 et AssertJ :
<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>
Le test ci-dessous vérifie deux comportements principaux : la ressource est toujours fermée même en cas d’erreur, et l’exception de close() est attachée à l’exception principale sous forme de suppressed.
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);
}
}
Le deuxième test utilise une lambda pour créer rapidement une AutoCloseable. C’est possible parce que AutoCloseable n’a qu’une seule abstract method, donc elle peut être utilisée comme une functional interface.
7. Erreurs courantes
7.1. Imbriquer des ressources dans le constructor
try (var in = new ObjectInputStream(new FileInputStream("orders.bin"))) {
// ...
}
Le constructeur de ObjectInputStream lit l’en-tête du stream et peut lever une IOException. À ce moment, la variable in n’a pas encore été assignée. Le FileInputStream intérieur a été ouvert mais ne sera jamais fermé. La façon sûre est de déclarer séparément chaque ressource :
try (var file = new FileInputStream("orders.bin");
var in = new ObjectInputStream(file)) {
// ...
}
7.2. Retourner une ressource en dehors du bloc try
public static Stream<String> loadOrderLines(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path)) {
return lines; // Erreur : le stream est fermé quand il arrive chez l'appelant
}
}
L’appelant recevra une IllegalStateException immédiatement en utilisant le stream. Vous avez deux façons de corriger cela. La première façon est de traiter toutes les données dans try, par exemple return lines.toList();. La deuxième façon est de retourner le stream et laisser l’appelant l’encapsuler dans try-with-resources.
Remarque : Seuls les
Streamassociés à une ressource I/O ont besoin d’être fermés, par exempleFiles.lines,Files.listouFiles.walk. Les Stream créés à partir deList.stream()ne nécessitent pas de fermeture.
7.3. Lever InterruptedException dans close()
La Javadoc de AutoCloseable conseille de ne pas lever InterruptedException depuis close(). La raison est que cette exception peut être supprimée, ce qui entraîne la perte de l’état d’interruption du thread. Si vous devez absolument attendre dans close(), capturez l’exception puis appelez Thread.currentThread().interrupt().
7.4. Ressource null
Déclarer try (var conn = findConnectionOrNull()) est valide. Si la valeur est null, Java omet simplement l’appel à close(). Cependant, c’est généralement un signe d’une mauvaise conception. Vous devriez utiliser Optional ou lever une exception depuis la factory.
7.5. Quand ne pas l’utiliser
Ce n’est pas parce qu’un objet implémente AutoCloseable qu’il doit être fermé immédiatement dans une méthode. Par exemple, dans Spring Boot, une DataSource ou un HttpClient partagé sont des beans qui vivent avec le contexte d’application. Spring les fermera quand le contexte s’arrête. Si vous encapsulez un bean partagé dans try-with-resources, vous le fermerez tandis que d’autres composants l’utilisent encore.
Utilisez try-with-resources uniquement pour les ressources que votre code crée et possède dans cette portée.
7.6. Conseil : ExecutorService en Java 21
À partir de Java 19, ExecutorService implémente AutoCloseable. La méthode close() attend que toutes les tâches se terminent. C’est très pratique quand vous utilisez des virtual 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("Envoyer la facture #" + invoiceId));
}
}
System.out.println("Toutes les factures ont été envoyées");
}
}
Le message “Toutes les factures ont été envoyées” est toujours affiché en dernier, car close() bloque jusqu’à ce que les trois tâches se terminent. L’ordre entre les trois factures n’est pas fixe.
8. Conclusion
try-with-resources rend votre code plus court et plus sûr que try-finally. Les ressources sont toujours fermées, dans l’ordre inverse de leur ouverture. L’exception originale n’est pas masquée, grâce au mécanisme de suppressed exception.
Quand vous écrivez votre propre classe AutoCloseable, vous devriez :
- restreindre les
throwsdeclose(); - rendre
close()idempotent ; - bloquer l’utilisation de l’objet après sa fermeture.
De plus, évitez d’imbriquer les ressources dans le constructeur, ne retournez pas les ressources en dehors du bloc try, et fermez uniquement les ressources que votre code possède réellement.