Souvent, votre application travaille avec des fichiers, des bases de données et divers objets de connexion. Généralement, ces sources de données externes sont appelées ressources. Dans de nombreux cas, vous ouvrez une connexion à la ressource, que ce soit via le réseau ou dans un système de fichiers. Vous lisez/écrivez ensuite les données souhaitées. Enfin, vous fermez la ressource pour indiquer que vous avez terminé.
Que se passe-t-il si vous ne fermez pas une ressource lorsque vous avez terminé? En résumé, beaucoup de mauvaises choses peuvent arriver. Si vous vous connectez à une base de données, vous pourriez épuiser toutes les connexions disponibles, ce qui signifie que personne ne pourra communiquer avec la base de données jusqu’à ce que vous libériez vos connexions. Bien que l’on entende souvent parler des fuites de mémoire provoquant des défaillances de programmes, une fuite de ressource est tout aussi grave et se produit lorsqu’un programme ne parvient pas à libérer ses connexions à une ressource, rendant la ressource inaccessible. Cela pourrait signifier que votre programme ne peut plus communiquer avec la base de données — ou, pire encore, que tous les programmes sont incapables d’accéder à la base de données!
Une ressource est généralement un fichier ou une base de données qui nécessite un flux ou une connexion pour lire ou écrire des données.
Introduction à Try-with-Resources
Examinons une méthode qui ouvre un fichier, lit les données et le ferme :
public void lireFichier(String fichier) {
FileInputStream is = null;
try {
is = new FileInputStream("monfichier.txt");
// Lire les données du fichier
} catch (IOException e) {
e.printStackTrace();
} finally {
if(is != null) {
try {
is.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
}
Wow, c’est une longue méthode! Pourquoi avons-nous deux blocs try et catch? Eh bien, les instructions de création du FileInputStream et la méthode close() incluent toutes deux des appels IOException vérifiés, et ceux-ci doivent être capturés dans la méthode ou relancés par la méthode. La moitié des lignes de code de cette méthode servent simplement à fermer une ressource. Et plus vous avez de ressources, plus ce type de code devient long. Par exemple, vous pouvez avoir plusieurs ressources qui doivent être fermées dans un ordre particulier. Vous ne voulez pas non plus qu’une exception causée par la fermeture d’une ressource empêche la fermeture d’une autre ressource.
Pour résoudre ce problème, Java inclut l’instruction try-with-resources pour fermer automatiquement toutes les ressources ouvertes dans une clause try. Cette fonctionnalité est également connue sous le nom de gestion automatique des ressources, car Java prend automatiquement en charge la fermeture.
Examinons le même exemple en utilisant une instruction try-with-resources:
public void lireFichier(String fichier) {
try (FileInputStream is = new FileInputStream("monfichier.txt")) {
// Lire les données du fichier
} catch (IOException e) {
e.printStackTrace();
}
}
Fonctionnellement, ils sont similaires, mais notre nouvelle version a deux fois moins de lignes. Plus important encore, en utilisant une instruction try-with-resources, nous garantissons que dès qu’une connexion sort de la portée, Java tentera de la fermer dans la même méthode.
En coulisses, le compilateur remplace un bloc try-with-resources par un bloc try et finally. Nous appelons ce bloc finally “caché” un bloc finally implicite car il est créé et utilisé automatiquement par le compilateur. Vous pouvez toujours créer un bloc finally défini par le programmeur lors de l’utilisation d’une instruction try-with-resources; sachez simplement que l’implicite sera appelé en premier.
Contrairement à la collecte des déchets, les ressources ne sont pas automatiquement fermées lorsqu’elles sortent de la portée. Par conséquent, il est recommandé de fermer les ressources dans le même bloc de code qui les ouvre. En utilisant une instruction try-with-resources pour ouvrir toutes vos ressources, cela se produit automatiquement.
Principes de base de Try-with-Resources
Une ou plusieurs ressources peuvent être ouvertes dans la clause try. Lorsque plusieurs ressources sont ouvertes, elles sont fermées dans l’ordre inverse de leur création. De plus, notez que des parenthèses sont utilisées pour répertorier ces ressources, et des points-virgules sont utilisés pour séparer les déclarations. Cela fonctionne comme la déclaration de plusieurs index dans une boucle for.
Le bloc catch est facultatif avec une instruction try-with-resources. Par exemple, nous pouvons réécrire l’exemple lireFichier() précédent de sorte que la méthode déclare l’exception pour la rendre encore plus courte:
public void lireFichier(String fichier) throws IOException {
try (FileInputStream is = new FileInputStream("monfichier.txt")) {
// Lire les données du fichier
}
}
Une instruction try-with-resources diffère d’une instruction try en ce que ni un bloc catch ni un bloc finally n’est requis, bien qu’un développeur puisse ajouter les deux. Le bloc finally implicite s’exécute avant tous ceux codés par le programmeur.
Construction d’instructions Try-with-Resources
Seules les classes qui implémentent l’interface AutoCloseable peuvent être utilisées dans une instruction try-with-resources. Par exemple, ce qui suit ne compile pas car String n’implémente pas l’interface AutoCloseable:
try (String reptile = "lézard") {} // NE COMPILE PAS
Hériter d’AutoCloseable nécessite l’implémentation d’une méthode close() compatible.
interface AutoCloseable {
public void close() throws Exception;
}
D’après vos études sur la redéfinition de méthode, cela signifie que la version implémentée de close() peut choisir de lancer Exception ou une sous-classe ou de ne pas lancer d’exceptions du tout.
Dans le reste de cette section, nous utilisons la classe de ressource personnalisée suivante qui imprime simplement un message lorsque la méthode close() est appelée:
public class MaClasseFichier implements AutoCloseable {
private final int num;
public MaClasseFichier(int num) { this.num = num; }
@Override public void close() {
System.out.println("Fermeture: " + num);
}
}
L’interface Closeable étend AutoCloseable. Puisque Closeable étend AutoCloseable, les deux sont pris en charge dans les instructions try-with-resources. La seule différence entre les deux est que la méthode close() de Closeable déclare IOException, tandis que la méthode close() d’AutoCloseable déclare Exception.
Déclaration de ressources
Bien que try-with-resources prenne en charge la déclaration de plusieurs variables, chaque variable doit être déclarée dans une instruction séparée. Par exemple, ce qui suit ne compile pas:
try (MaClasseFichier is = new MaClasseFichier(1), // NE COMPILE PAS
os = new MaClasseFichier(2)) {
}
try (MaClasseFichier ab = new MaClasseFichier(1), // NE COMPILE PAS
MaClasseFichier cd = new MaClasseFichier(2)) {
}
Le premier exemple ne compile pas car il manque le type de données et utilise une virgule (,) au lieu d’un point-virgule (;). Le deuxième exemple ne compile pas car il utilise également une virgule (,) au lieu d’un point-virgule (;). Chaque ressource doit inclure le type de données et être séparée par un point-virgule (;).
Vous pouvez déclarer une ressource en utilisant var comme type de données dans une instruction try-with-resources, puisque les ressources sont des variables locales.
try (var f = new BufferedInputStream(new FileInputStream("it.txt"))) {
// Traiter le fichier
}
La déclaration de ressources est une situation courante où l’utilisation de var est très utile, car elle raccourcit la ligne de code déjà longue.
Portée de Try-with-Resources
Les ressources créées dans la clause try ne sont à portée que dans le bloc try. C’est une autre façon de se rappeler que le finally implicite s’exécute avant tout bloc catch/finally que vous codez vous-même. La fermeture implicite a déjà été exécutée, et la ressource n’est plus disponible. Voyez-vous pourquoi les lignes 6 et 8 ne compilent pas dans cet exemple?
try (Scanner s = new Scanner(System.in)) {
s.nextLine();
} catch(Exception e) {
s.nextInt(); // NE COMPILE PAS
} finally {
s.nextInt(); // NE COMPILE PAS
}
Le problème est que Scanner est sorti de la portée à la fin de la clause try. Les lignes 6 et 8 n’y ont pas accès. C’est une fonctionnalité intéressante. Vous ne pouvez pas utiliser accidentellement un objet qui a été fermé. Dans une instruction try traditionnelle, la variable doit être déclarée avant l’instruction try pour que les blocs try et finally puissent y accéder, ce qui a l’effet désagréable de mettre la variable à portée pour le reste de la méthode, vous invitant à l’appeler par accident.
Ordre des opérations
Lorsque vous travaillez avec des instructions try-with-resources, il est important de savoir que les ressources sont fermées dans l’ordre inverse de leur création. En utilisant notre MaClasseFichier personnalisée, pouvez-vous déterminer ce que cette méthode imprime?
public static void main(String... xyz) {
try (MaClasseFichier lecteurLivre = new MaClasseFichier(1);
MaClasseFichier lecteurFilm = new MaClasseFichier(2)) {
System.out.println("Bloc Try");
throw new RuntimeException();
} catch (Exception e) {
System.out.println("Bloc Catch");
} finally {
System.out.println("Bloc Finally");
}
}
La sortie est la suivante:
Bloc Try Fermeture: 2 Fermeture: 1 Bloc Catch Bloc Finally
Assurez-vous de comprendre pourquoi la méthode imprime les instructions dans cet ordre. Rappelez-vous, les ressources sont fermées dans l’ordre inverse de leur déclaration, et le finally implicite est exécuté avant le finally défini par le programmeur.
Application effective de final
Bien que les ressources soient souvent créées dans l’instruction try-with-resources, il est possible de les déclarer à l’avance, à condition qu’elles soient marquées final ou effectivement final. La syntaxe utilise le nom de la ressource à la place de la déclaration de ressource, séparé par un point-virgule (;). Essayons un autre exemple:
public static void main(String... xyz) {
final var lecteurLivre = new MaClasseFichier(4);
MaClasseFichier lecteurFilm = new MaClasseFichier(5);
try (lecteurLivre;
var lecteurTv = new MaClasseFichier(6);
lecteurFilm) {
System.out.println("Bloc Try");
} finally {
System.out.println("Bloc Finally");
}
}
Prenons cela ligne par ligne. La ligne 2 déclare une variable final lecteurLivre, tandis que la ligne 3 déclare une variable effectivement final lecteurFilm. Ces deux ressources peuvent être utilisées dans une instruction try-with-resources. Nous savons que lecteurFilm est effectivement final car c’est une variable locale qui n’est assignée qu’une seule fois. Rappelez-vous, le test pour effectivement final est que si nous insérons le mot-clé final lors de la déclaration de la variable, le code compile toujours.
Les lignes 5 et 7 utilisent la nouvelle syntaxe pour déclarer des ressources dans une instruction try-with-resources, en utilisant simplement le nom de la variable et en séparant les ressources par un point-virgule (;). La ligne 6 utilise la syntaxe normale pour déclarer une nouvelle ressource dans la clause try.
À l’exécution, le code imprime ce qui suit:
Bloc Try Fermeture: 5 Fermeture: 6 Fermeture: 4 Bloc Finally
Si vous rencontrez une question qui utilise une instruction try-with-resources avec une variable non déclarée dans la clause try, assurez-vous qu’elle est effectivement final. Par exemple, ce qui suit ne compile pas:
var writer = Files.newBufferedWriter(path);
try (writer) { // NE COMPILE PAS
writer.append("Bienvenue au zoo!");
}
writer = null;
La variable writer est réaffectée à la ligne 4, ce qui fait que le compilateur ne la considère pas comme effectivement final. Comme ce n’est pas une variable effectivement final, elle ne peut pas être utilisée dans une instruction try-with-resources à la ligne 2.
L’autre endroit où l’on pourrait vous piéger est l’accès à une ressource après sa fermeture. Considérez ce qui suit:
var writer = Files.newBufferedWriter(path);
writer.append("Cette écriture est autorisée mais c'est vraiment une mauvaise idée!");
try (writer) {
writer.append("Bienvenue au zoo!");
}
writer.append("Cette écriture échouera!"); // IOException
Ce code compile mais lance une exception à la ligne 5 avec le message Stream closed. Bien qu’il soit possible d’écrire dans la ressource avant l’instruction try-with-resources, ce n’est pas le cas après.
Comprendre les exceptions supprimées
Nous concluons notre discussion sur les exceptions avec probablement le sujet le plus déroutant: les exceptions supprimées. Que se passe-t-il si la méthode close() lance une exception? Essayons un exemple illustratif:
public class CageDindons implements AutoCloseable {
public void close() {
System.out.println("Fermer la porte");
}
public static void main(String[] args) {
try (var t = new CageDindons()) {
System.out.println("Mettre les dindons dedans");
}
}
}
Si la CageDindons ne se ferme pas, les dindons pourraient tous s’échapper. Clairement, nous devons gérer une telle condition. Nous savons déjà que les ressources sont fermées avant que les blocs catch codés par le programmeur ne soient exécutés. Cela signifie que nous pouvons attraper l’exception lancée par close() si nous le souhaitons. Alternativement, nous pouvons permettre à l’appelant de s’en occuper.
Étendons notre exemple avec une nouvelle implémentation CageDindonsBloquée, présentée ici:
public class CageDindonsBloquée implements AutoCloseable {
public void close() throws IllegalStateException {
throw new IllegalStateException("La porte de la cage ne se ferme pas");
}
public static void main(String[] args) {
try (CageDindonsBloquée t = new CageDindonsBloquée()) {
System.out.println("Mettre les dindons dedans");
} catch (IllegalStateException e) {
System.out.println("Attrapé: " + e.getMessage());
}
}
}
La méthode close() est automatiquement appelée par try-with-resources. Elle lance une exception, qui est capturée par notre bloc catch et imprime ce qui suit:
Attrapé: La porte de la cage ne se ferme pas
Cela semble assez raisonnable. Que se passe-t-il si le bloc try lance également une exception? Lorsque plusieurs exceptions sont lancées, toutes sauf la première sont appelées exceptions supprimées. L’idée est que Java traite la première exception comme la principale et y ajoute toutes celles qui surviennent lors de la fermeture automatique.
Que pensez-vous que cette implémentation de notre méthode main() affiche?
public static void main(String[] args) {
try (CageDindonsBloquée t = new CageDindonsBloquée()) {
throw new IllegalStateException("Les dindons se sont enfuis");
} catch (IllegalStateException e) {
System.out.println("Attrapé: " + e.getMessage());
for (Throwable t: e.getSuppressed())
System.out.println("Supprimée: "+t.getMessage());
}
}
La ligne 3 lance l’exception principale. À ce stade, la clause try se termine, et Java appelle automatiquement la méthode close(). Notre implémentation CageDindonsBloquée lance une IllegalStateException, qui est ajoutée comme exception supprimée. Puis la ligne 4 capture l’exception principale. La ligne 5 imprime le message pour l’exception principale. Les lignes 6 et 7 itèrent à travers toutes les exceptions supprimées et les impriment. Le programme imprime ce qui suit:
Attrapé: Les dindons se sont enfuis Supprimée: La porte de la cage ne se ferme pas
Gardez à l’esprit que le bloc catch recherche des correspondances sur l’exception principale. Que pensez-vous que ce code imprime?
public static void main(String[] args) {
try (CageDindonsBloquée t = new CageDindonsBloquée()) {
throw new RuntimeException("Les dindons se sont enfuis");
} catch (IllegalStateException e) {
System.out.println("attrapé: " + e.getMessage());
}
}
La ligne 3 lance à nouveau l’exception principale. Java appelle la méthode close() et ajoute une exception supprimée. La ligne 4 capturerait l’IllegalStateException. Cependant, nous n’en avons pas. L’exception principale est une RuntimeException. Comme elle ne correspond pas à la clause catch, l’exception est lancée à l’appelant. Finalement, la méthode main() afficherait quelque chose comme:
Exception in thread "main" java.lang.RuntimeException: Les dindons se sont enfuis at CageDindonsBloquée.main(CageDindonsBloquée.java:3) Supprimée: java.lang.IllegalStateException: La porte de la cage ne se ferme pas at CageDindonsBloquée.close(CageDindonsBloquée.java:3) at CageDindonsBloquée.main(CageDindonsBloquée.java:4)
Java se souvient des exceptions supprimées qui vont avec une exception principale même si vous ne les gérez pas dans le code.
Si plus de deux ressources lancent une exception, la première à être lancée devient l’exception principale, et les autres sont regroupées comme exceptions supprimées. Et puisque les ressources sont fermées dans l’ordre inverse de leur déclaration, l’exception principale sera sur la dernière ressource déclarée qui lance une exception.
Gardez à l’esprit que les exceptions supprimées ne s’appliquent qu’aux exceptions lancées dans la clause try. L’exemple suivant ne lance pas d’exception supprimée:
public static void main(String[] args) {
try (CageDindonsBloquée t = new CageDindonsBloquée()) {
throw new IllegalStateException("Les dindons se sont enfuis");
} finally {
throw new RuntimeException("et nous n'avons pas pu les retrouver");
}
}
La ligne 3 lance une exception. Ensuite, Java essaie de fermer la ressource et ajoute une exception supprimée. Maintenant, nous avons un problème. Le bloc finally s’exécute après tout cela. Comme la ligne 5 lance également une exception, l’exception précédente de la ligne 3 est perdue, le code imprimant:
Exception in thread "main" java.lang.RuntimeException: et nous n'avons pas pu les retrouver at CageDindonsBloquée.main(CageDindonsBloquée.java:5)
Cela a toujours été et continue d’être une mauvaise pratique de programmation. Nous ne voulons pas perdre des exceptions!