Idiome correct pour la gestion de plusieurs ressources chaînées dans un bloc try-with-resources?

La syntaxe Java 7 try-with-resources (également connue sous le nom de bloc ARM ( Automatic Resource Management )) est simple, AutoCloseable et simple lorsque vous utilisez une AutoCloseable ressource AutoCloseable . Cependant, je ne suis pas sûr de savoir quel est le langage correct lorsque j’ai besoin de déclarer plusieurs ressources dépendantes les unes des autres, par exemple un FileWriter et un BufferedWriter qui l’ FileWriter . Bien entendu, cette question concerne tous les cas où certaines ressources AutoCloseable sont AutoCloseable , pas seulement ces deux classes spécifiques.

Je suis venu avec les trois alternatives suivantes:

1)

L’idiome naïf que j’ai vu est de ne déclarer que le wrapper de niveau supérieur dans la variable gérée par ARM:

 static void printToFile1(Ssortingng text, File file) { try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { bw.write(text); } catch (IOException ex) { // handle ex } } 

C’est gentil et court, mais c’est cassé. Le FileWriter sous-jacent FileWriter pas déclaré dans une variable, il ne sera jamais fermé directement dans le bloc finally généré. Il ne sera fermé que par la méthode de close de BufferedWriter . Le problème est que si une exception est lancée par le constructeur de la bw , sa close ne sera pas appelée et par conséquent, le FileWriter sous-jacent FileWriter sera pas fermé .

2)

 static void printToFile2(Ssortingng text, File file) { try (FileWriter fw = new FileWriter(file); BufferedWriter bw = new BufferedWriter(fw)) { bw.write(text); } catch (IOException ex) { // handle ex } } 

Ici, les ressources sous-jacentes et enveloppantes sont déclarées dans les variables gérées par ARM, elles seront donc toutes deux fermées, mais le sous- fw.close() sous-jacent sera appelé deux fois : bw.close() .

Cela ne devrait pas être un problème pour ces deux classes spécifiques qui implémentent toutes les deux Closeable (qui est un sous-type de AutoCloseable ), dont le contrat stipule que plusieurs appels à close sont autorisés:

Ferme ce stream et libère toutes les ressources système associées. Si le stream est déjà fermé, l’invocation de cette méthode n’a aucun effet.

Cependant, dans un cas général, je peux avoir des ressources qui implémentent uniquement AutoCloseable (et non Closeable ), ce qui ne garantit pas que la close peut être appelée plusieurs fois:

Notez que contrairement à la méthode close de java.io.Closeable, cette méthode close n’a pas besoin d’être idempotente. En d’autres termes, appeler cette méthode close plus d’une fois peut avoir des effets secondaires visibles, contrairement à Closeable.close, qui ne doit avoir aucun effet s’il est appelé plus d’une fois. Cependant, les implémenteurs de cette interface sont fortement encouragés à rendre leurs méthodes proches idempotentes.

3)

 static void printToFile3(Ssortingng text, File file) { try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); } catch (IOException ex) { // handle ex } } 

Cette version devrait être théoriquement correcte, car seule la fw représente une ressource réelle qui doit être nettoyée. Le bw ne contient aucune ressource, il ne délègue que le fw , il devrait donc suffire de fermer le fw sous-jacent.

D’un autre côté, la syntaxe est un peu irrégulière et, de plus, Eclipse émet un avertissement, qui est, à mon avis, une fausse alarme, mais il s’agit toujours d’un avertissement à traiter:

Fuite de ressource: ‘bw’ n’est jamais fermé


Alors, quelle approche choisir? Ou ai-je manqué un autre idiome qui est le bon ?

    Voici ma version des alternatives:

    1)

     try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { bw.write(text); } 

    Pour moi, la meilleure chose à faire depuis Java avec C ++ traditionnel il y a 15 ans était que vous pouviez faire confiance à votre programme. Même si les choses tournent mal et se passent mal, ce que je fais souvent, je veux que le rest du code soit axé sur le meilleur comportement et sur l’odeur des roses. En effet, le BufferedWriter pourrait lancer une exception ici. Ne plus avoir de mémoire ne serait pas inhabituel, par exemple. Pour d’autres décorateurs, savez-vous laquelle des classes d’encapsuleur java.io lance une exception vérifiée à partir de leurs constructeurs? Je ne. Ne comprend pas bien la compréhension du code si vous vous fiez à ce genre de connaissances obscures.

    Il y a aussi la “destruction”. S’il y a une condition d’erreur, vous ne voudrez probablement pas vider les déchets dans un fichier qui doit être supprimé (code correspondant à celui qui n’est pas affiché). Bien sûr, la suppression du fichier est également une autre opération intéressante à effectuer en tant que traitement des erreurs.

    En général, vous voulez que les blocs soient aussi courts et fiables que possible. L’ajout de flushes n’aide pas cet objective. Pour de nombreuses versions, certaines des classes de mise en mémoire tampon du JDK avaient un bogue dans lequel une exception de flush à la close causée par la close de l’object décoré n’était pas appelée. Bien que cela ait été corrigé depuis un certain temps, attendez-vous à ce qu’il soit réalisé par d’autres implémentations.

    2)

     try ( FileWriter fw = new FileWriter(file); BufferedWriter bw = new BufferedWriter(fw) ) { bw.write(text); } 

    Nous vidons toujours le bloc implicite finalement (maintenant avec la close répétée – cela devient pire quand vous ajoutez plus de décorateurs), mais la construction est sûre et nous devons implicitement bloquer les blocs même si un flush échouera.

    3)

     try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); } 

    Il y a un bug ici. Devrait être:

     try (FileWriter fw = new FileWriter(file)) { BufferedWriter bw = new BufferedWriter(fw); bw.write(text); bw.flush(); } 

    Certains décorateurs mal mis en œuvre sont en fait des ressources et devront être fermés de manière fiable. De plus, certains stream doivent être fermés d’une manière particulière (peut-être font-ils de la compression et doivent-ils écrire des bits pour terminer, et ne peuvent pas simplement tout vider.

    Verdict

    Bien que 3 soit une solution techniquement supérieure, les raisons du développement logiciel en font le meilleur choix. Cependant, try-with-resource est toujours un correctif inadéquat et vous devriez vous en tenir à l’ idiome Execute Around , qui devrait avoir une syntaxe plus claire avec des fermetures dans Java SE 8.

    Le premier style est celui suggéré par Oracle . BufferedWriter ne BufferedWriter pas les exceptions vérifiées. Par conséquent, si une exception est levée, le programme n’est pas censé s’en sortir, ce qui rend la récupération de la ressource essentiellement inutile.

    Surtout parce que cela pourrait se produire dans un thread, avec le thread en train de mourir, mais le programme continue toujours – par exemple, il y avait une panne de mémoire temporaire qui n’était pas assez longue pour sérieusement affecter le rest du programme. Il s’agit d’un cas assez particulier, et si cela se produit assez souvent pour que les ressources fuient, l’essai avec les ressources est le moindre de vos problèmes.

    Option 4

    Modifiez vos ressources pour qu’elles soient fermables, et non AutoClosable si vous le pouvez. Le fait que les constructeurs puissent être chaînés implique qu’il n’est pas rare de fermer la ressource deux fois. (C’était vrai avant ARM aussi).

    Option 5

    N’utilisez pas ARM et codez très soigneusement pour vous assurer que close () n’est pas appelée deux fois!

    Option 6

    N’utilisez pas ARM et vos appels last () close dans un try / catch eux-mêmes.

    Pourquoi je ne pense pas que ce problème soit unique à ARM

    Dans tous ces exemples, les appels finally close () doivent être dans un bloc catch. Laissé pour la lisibilité.

    Pas bon car fw peut être fermé deux fois. (ce qui est bien pour FileWriter mais pas dans votre exemple hypothétique):

     FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if ( fw != null ) fw.close(); if ( bw != null ) bw.close(); } 

    Pas bon car fw n’est pas fermé si exception sur la construction d’un BufferedWriter. (encore une fois, cela ne peut pas arriver, mais dans votre exemple hypothétique):

     FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if ( bw != null ) bw.close(); } 

    Je voulais juste me baser sur la suggestion de Jeanne Boyarsky de ne pas utiliser ARM mais en veillant à ce que FileWriter soit toujours fermé une seule fois. Ne pensez pas qu’il y a des problèmes ici …

     FileWriter fw = null; BufferedWriter bw = null; try { fw = new FileWriter(file); bw = new BufferedWriter(fw); bw.write(text); } finally { if (bw != null) bw.close(); else if (fw != null) fw.close(); } 

    Je suppose que puisque ARM est juste du sucre syntaxique, nous ne pouvons pas toujours l’utiliser pour remplacer finalement les blocs. Tout comme nous ne pouvons pas toujours utiliser une boucle for-each pour faire quelque chose qui est possible avec les iterators.

    En accord avec les commentaires précédents: le plus simple est d’utiliser (2) les ressources Closeable et de les déclarer dans la clause try-with-resources. Si vous ne disposez que de la fonction AutoCloseable , vous pouvez les AutoCloseable dans une autre classe (nestede) qui vérifie simplement que close est appelée une seule fois (Facade Pattern), par exemple en ayant private bool isClosed; . En pratique, même Oracle (1) enchaîne les constructeurs et ne gère pas correctement les exceptions à travers la chaîne.

    Vous pouvez également créer manuellement une ressource chaînée en utilisant une méthode de fabrique statique. Cela encapsule la chaîne et gère le nettoyage si elle échoue à mi-chemin:

     static BufferedWriter createBufferedWriterFromFile(File file) throws IOException { // If constructor throws an exception, no resource acquired, so no release required. FileWriter fileWriter = new FileWriter(file); try { return new BufferedWriter(fileWriter); } catch (IOException newBufferedWriterException) { try { fileWriter.close(); } catch (IOException closeException) { // Exceptions in cleanup code are secondary to exceptions in primary code (body of try), // as in try-with-resources. newBufferedWriterException.addSuppressed(closeException); } throw newBufferedWriterException; } } 

    Vous pouvez ensuite l’utiliser comme une ressource unique dans une clause try-with-resources:

     try (BufferedWriter writer = createBufferedWriterFromFile(file)) { // Work with writer. } 

    La complexité vient du traitement des exceptions multiples; sinon c’est juste “des ressources proches que vous avez acquises jusqu’ici”. Une pratique courante semble être d’initialiser la variable fileWriter l’object qui contient la ressource à null (ici fileWriter ), puis d’inclure une vérification null dans le nettoyage, mais cela semble inutile: si le constructeur échoue, il n’ya rien à nettoyer up, nous pouvons donc simplement laisser cette exception se propager, ce qui simplifie un peu le code.

    Vous pourriez probablement le faire de manière générique:

     static  T createChainedResource(V v) throws Exception { // If constructor throws an exception, no resource acquired, so no release required. U u = new U(v); try { return new T(u); } catch (Exception newTException) { try { u.close(); } catch (Exception closeException) { // Exceptions in cleanup code are secondary to exceptions in primary code (body of try), // as in try-with-resources. newTException.addSuppressed(closeException); } throw newTException; } } 

    De même, vous pouvez enchaîner trois ressources, etc.

    En tant que mathématique de côté, vous pourriez même enchaîner trois fois en enchaînant deux ressources à la fois, et cela serait associatif, ce qui signifie que vous obtiendriez le même object (car les constructeurs sont associatifs), et les mêmes exceptions en cas d’échec dans l’un des constructeurs. En supposant que vous avez ajouté un S à la chaîne ci-dessus (vous commencez donc avec un V et vous terminez par un S , en appliquant tour à tour U , T et S ), vous obtenez la même chose si correspondant à (ST) U , ou si vous avez d’abord chaîné T et U , alors S , correspondant à S (TU) . Cependant, il serait plus clair de simplement écrire une chaîne explicite en trois parties dans une seule fonction d’usine.

    Comme vos ressources sont nestedes, vos clauses try-with devraient également être:

     try (FileWriter fw=new FileWriter(file)) { try (BufferedWriter bw=new BufferedWriter(fw)) { bw.write(text); } catch (IOException ex) { // handle ex } } catch (IOException ex) { // handle ex } 

    Je dirais ne pas utiliser ARM et continuer avec Closeable. Utilisez la méthode comme,

     public void close(Closeable... closeables) { for (Closeable closeable: closeables) { try { closeable.close(); } catch (IOException e) { // you can't much for this } } } 

    Aussi, vous devriez envisager d’appeler close de BufferedWriter car il ne s’agit pas seulement de déléguer la proximité à FileWriter , mais il effectue un nettoyage comme flushBuffer .

    Ma solution consiste à effectuer une refactorisation de la “méthode d’extraction”, comme suit:

     static AutoCloseable writeFileWriter(FileWriter fw, Ssortingng txt) throws IOException{ final BufferedWriter bw = new BufferedWriter(fw); bw.write(txt); return new AutoCloseable(){ @Override public void close() throws IOException { bw.flush(); } }; } 

    printToFile peut être écrit soit

     static void printToFile(Ssortingng text, File file) { try (FileWriter fw = new FileWriter(file)) { AutoCloseable w = writeFileWriter(fw, text); w.close(); } catch (Exception ex) { // handle ex } } 

    ou

     static void printToFile(Ssortingng text, File file) { try (FileWriter fw = new FileWriter(file); AutoCloseable w = writeFileWriter(fw, text)){ } catch (Exception ex) { // handle ex } } 

    Pour les concepteurs de AutoClosable classes, je leur suggère d’étendre l’interface AutoClosable avec une méthode supplémentaire pour supprimer la fermeture. Dans ce cas, nous pouvons alors contrôler manuellement le comportement de fermeture.

    Pour les concepteurs de langues, la leçon est que l’ajout d’une nouvelle fonctionnalité peut signifier en append beaucoup d’autres. Dans ce cas Java, la fonctionnalité ARM fonctionnera mieux avec un mécanisme de transfert de propriété des ressources.

    METTRE À JOUR

    À l’origine, le code ci-dessus nécessite @SuppressWarning car BufferedWriter à l’intérieur de la fonction nécessite close() .

    Comme suggéré par un commentaire, si flush() doit être appelé avant de fermer le script, nous devons le faire avant toute return (implicite ou explicite) de return dans le bloc try. Il n’y a actuellement aucun moyen de garantir que l’appelant fasse cela, donc cela doit être documenté pour writeFileWriter .

    MISE À JOUR ENCORE

    La mise à jour ci-dessus rend @SuppressWarning inutile car elle nécessite que la fonction retourne la ressource à l’appelant, de sorte qu’elle ne soit pas nécessairement fermée. Malheureusement, cela nous ramène au début de la situation: l’avertissement est maintenant redirigé vers le côté appelant.

    Donc, pour résoudre ce problème correctement, nous avons besoin d’un AutoClosable personnalisé qui, chaque fois qu’il se ferme, soulignera BufferedWriter flush() ed. En fait, cela nous montre une autre façon de contourner l’avertissement, puisque le BufferWriter n’est jamais fermé dans aucun cas.