La syntaxe Java 7 try-with-resources (également connue sous le nom de bloc ARM ( Automatic Resource Management )) est agréable, courte et simple lors de l'utilisation d'une seule AutoCloseable
ressource. Cependant, je ne sais pas quelle est l'idiome correct lorsque je dois déclarer plusieurs ressources qui dépendent les unes des autres, par exemple a FileWriter
et aBufferedWriter
qui l'enveloppent. Bien entendu, cette question concerne tous les cas où certaines AutoCloseable
ressources sont encapsulées, pas seulement ces deux classes spécifiques.
J'ai proposé les trois alternatives suivantes:
1)
L'idiome naïf que j'ai vu est de déclarer uniquement le wrapper de niveau supérieur dans la variable gérée par ARM:
static void printToFile1(String text, File file) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
bw.write(text);
} catch (IOException ex) {
// handle ex
}
}
C'est joli et court, mais c'est cassé. Comme le sous FileWriter
- jacent n'est pas déclaré dans une variable, il ne sera jamais fermé directement dans le finally
bloc généré . Il sera fermé uniquement par la close
méthode de l'emballage BufferedWriter
. Le problème est que si une exception est levée depuis le bw
constructeur de s, close
elle ne sera pas appelée et donc le sous-jacent FileWriter
ne sera pas fermé .
2)
static void printToFile2(String text, File file) {
try (FileWriter fw = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fw)) {
bw.write(text);
} catch (IOException ex) {
// handle ex
}
}
Ici, la ressource sous-jacente et la ressource d'encapsulation sont déclarées dans les variables gérées par ARM, donc les deux seront certainement fermées, mais le sous-jacent fw.close()
sera appelé deux fois : non seulement directement, mais également via l'encapsulation 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 flux et libère toutes les ressources système qui lui sont associées. Si le flux 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 queclose
puissent être appelées 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 un effet secondaire visible, contrairement à Closeable.close qui doit n'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(String 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 seul le fw
représente une ressource réelle qui doit être nettoyée. Le bw
lui-même ne contient aucune ressource, il ne délègue qu'au fw
, il devrait donc suffire de fermer uniquement le sous-jacent fw
.
D'un autre côté, la syntaxe est un peu irrégulière et aussi, Eclipse émet un avertissement, qui je crois est une fausse alerte, mais c'est quand même un avertissement auquel il faut faire face:
Fuite de ressources: 'bw' n'est jamais fermé
Alors, quelle approche adopter? Ou ai-je manqué un autre idiome qui est le bon ?
la source
public BufferedWriter(Writer out, int sz)
peut lancer un fichierIllegalArgumentException
. De plus, je peux étendre BufferedWriter avec une classe qui lèverait quelque chose de son constructeur ou créerait un wrapper personnalisé dont j'ai besoin.BufferedWriter
constructeur peut facilement lever une exception.OutOfMemoryError
est probablement le plus courant car il alloue une bonne partie de la mémoire pour le tampon (bien qu'il puisse indiquer que vous souhaitez redémarrer l'ensemble du processus). / Vous avez besoin deflush
votreBufferedWriter
si vous ne fermez pas et que vous souhaitez conserver le contenu (généralement uniquement le cas sans exception).FileWriter
ramasse tout ce qui se trouve être le codage de fichier "par défaut" - il vaut mieux être explicite.Réponses:
Voici mon avis sur les alternatives:
1)
Pour moi, la meilleure chose à faire en Java depuis le C ++ traditionnel il y a 15 ans était que vous pouviez faire confiance à votre programme. Même si les choses sont dans la boue et vont mal, ce qu'elles font souvent, je veux que le reste du code soit sur le meilleur comportement et l'odeur des roses. En effet, le
BufferedWriter
pourrait jeter une exception ici. Manquer de mémoire ne serait pas inhabituel, par exemple. Pour les autres décorateurs, savez-vous lequel desjava.io
classes wrapper lancent une exception vérifiée de leurs constructeurs? Je ne. La compréhension du code n'est pas très bonne 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 voulez probablement pas vider les ordures dans un fichier qui doit être supprimé (code pour cela non affiché). Bien que, bien sûr, la suppression du fichier soit également une autre opération intéressante à effectuer en tant que gestion des erreurs.
En règle générale, vous voulez que les
finally
blocs soient aussi courts et fiables que possible. L'ajout de flushs n'aide pas cet objectif. Pour de nombreuses versions, certaines des classes de mise en mémoire tampon dans le JDK avaient un bogue où une exception de l'flush
intérieurclose
provoquéeclose
sur l'objet décoré n'était pas appelée. Bien que cela ait été corrigé depuis un certain temps, attendez-vous à d'autres implémentations.2)
Nous sommes toujours en train de rincer le bloc implicite finally (maintenant avec
close
- cela s'aggrave à mesure que vous ajoutez plus de décorateurs), mais la construction est sûre et nous devons finalement bloquer implicitement afin que même un échecflush
n'empêche pas la libération de ressources.3)
Il y a un bug ici. Devrait être:
Certains décorateurs mal implémentés sont en fait des ressources et devront être fermés de manière fiable. De plus, certains flux peuvent avoir besoin d'ê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, des raisons de développement logiciel font de 2 le meilleur choix. Cependant, try-with-resource est toujours une solution inadéquate et vous devez vous en tenir à l' idiome Execute Around , qui devrait avoir une syntaxe plus claire avec des fermetures dans Java SE 8.
la source
Le premier style est celui suggéré par Oracle .
BufferedWriter
ne lève pas d'exceptions vérifiées, donc si une exception est levée, le programme ne devrait pas s'en remettre, ce qui rend la récupération des ressources pratiquement inutile.Surtout parce que cela pouvait se produire dans un thread, avec la mort du thread mais le programme continuait toujours - disons, il y avait une panne de mémoire temporaire qui n'était pas assez longue pour sérieusement affecter le reste du programme. C'est un cas plutôt compliqué, cependant, et si cela arrive assez souvent pour faire de la fuite de ressources un problème, l'essai avec des ressources est le moindre de vos problèmes.
la source
Option 4
Modifiez vos ressources pour qu'elles soient fermables et non automatiquement 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 également vrai avant ARM.) Plus d'informations ci-dessous.
Option 5
N'utilisez pas ARM et code très soigneusement pour vous assurer que close () n'est pas appelé deux fois!
Option 6
N'utilisez pas ARM et ayez vos appels enfin close () dans un try / catch eux-mêmes.
Pourquoi je ne pense pas que ce problème soit propre à 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 convient à FileWriter mais pas dans votre exemple hypothétique):
Pas bon car fw n'est pas fermé en cas d'exception lors de la construction d'un BufferedWriter. (encore une fois, cela ne peut pas arriver, mais dans votre exemple hypothétique):
la source
Je voulais juste m'appuyer sur la suggestion de Jeanne Boyarsky de ne pas utiliser ARM mais de m'assurer que FileWriter est toujours fermé exactement une fois. Ne pensez pas qu'il y a des problèmes ici ...
Je suppose que comme ARM n'est que du sucre syntaxique, nous ne pouvons pas toujours l'utiliser pour remplacer finalement des blocs. Tout comme nous ne pouvons pas toujours utiliser une boucle for-each pour faire quelque chose qui est possible avec les itérateurs.
la source
try
et vosfinally
blocs lancent des exceptions, cette construction perdra la première (et potentiellement la plus utile).Pour être d'accord avec les commentaires précédents: le plus simple est (2) d'utiliser les
Closeable
ressources et de les déclarer dans l'ordre dans la clause try-with-resources. Si vous ne l'avez queAutoCloseable
, vous pouvez les envelopper dans une autre classe (imbriquée) qui vérifie juste et quiclose
n'est appelée qu'une seule fois (Facade Pattern), par exemple en ayantprivate bool isClosed;
. En pratique, même Oracle enchaîne simplement (1) les constructeurs et ne gère pas correctement les exceptions à mi-chemin de la chaîne.Vous pouvez également créer manuellement une ressource chaînée à l'aide d'une méthode de fabrique statique; cela encapsule la chaîne et gère le nettoyage s'il échoue en partie:
Vous pouvez ensuite l'utiliser comme ressource unique dans une clause try-with-resources:
La complexité vient de la gestion de plusieurs exceptions; sinon, il s'agit simplement de "fermer les ressources que vous avez acquises jusqu'à présent". Une pratique courante semble être de commencer par initialiser la variable qui contient l'objet qui contient la ressource
null
(icifileWriter
), puis d'inclure une vérification nulle dans le nettoyage, mais cela semble inutile: si le constructeur échoue, il n'y a rien à nettoyer, nous pouvons donc laisser cette exception se propager, ce qui simplifie un peu le code.Vous pourriez probablement le faire de manière générique:
De même, vous pouvez chaîner trois ressources, etc.
D'un point de vue mathématique, vous pouvez même enchaîner trois fois en chaînant deux ressources à la fois, et ce serait associatif, ce qui signifie que vous obtiendriez le même objet en cas de succès (car les constructeurs sont associatifs), et les mêmes exceptions en cas d'échec dans l'un des constructeurs. En supposant que vous ayez ajouté un S à la chaîne ci-dessus (donc vous commencez par un V et vous terminez par un S , en appliquant tour à tour U , T et S ), vous obtenez la même chose soit si vous enchaînez d'abord S et T , puis U , correspondant à (ST) U , ou si vous avez d'abord enchaîné T et U , alorsS , correspondant à S (TU) . Cependant, il serait plus clair d'écrire simplement une chaîne triple explicite dans une seule fonction d'usine.
la source
try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }
?Étant donné que vos ressources sont imbriquées, vos clauses try-with doivent également être:
la source
close
sera appelé deux fois.Je dirais de ne pas utiliser ARM et de continuer avec Closeable. Utilisez une méthode comme,
Vous devriez également envisager d'appeler close of
BufferedWriter
car il ne s'agit pas simplement de déléguer la proximité deFileWriter
, mais il effectue un nettoyage commeflushBuffer
.la source
Ma solution est de faire un refactoring "méthode d'extraction", comme suit:
printToFile
peut être écrit soitou
Pour les concepteurs de bibliothèques de classes, je leur proposerai d'étendre l'
AutoClosable
interface 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 langage, la leçon est que l'ajout d'une nouvelle fonctionnalité peut signifier en ajouter beaucoup d'autres. Dans ce cas Java, la fonctionnalité ARM fonctionnera évidemment mieux avec un mécanisme de transfert de propriété des ressources.
METTRE À JOUR
À l'origine, le code ci-dessus nécessite
@SuppressWarning
car l'BufferedWriter
intérieur de la fonction nécessiteclose()
.Comme suggéré par un commentaire, si
flush()
nous devons être appelés avant de fermer l'écrivain, nous devons le faire avant toutreturn
instruction (implicite ou explicite) à l'intérieur du bloc try. Il n'y a actuellement aucun moyen de garantir que l'appelant fasse cela, je pense, donc cela doit être documenté pourwriteFileWriter
.MISE À JOUR À NOUVEAU
La mise à jour ci-dessus fait
@SuppressWarning
inutile car elle nécessite que la fonction retourne la ressource à l'appelant, elle-même n'a donc pas besoin d'être fermée. Malheureusement, cela nous ramène au début de la situation: l'avertissement est maintenant renvoyé du côté de l'appelant.Donc, pour résoudre correctement cela, nous avons besoin d'un personnalisé
AutoClosable
qui, chaque fois qu'il se ferme, le soulignementBufferedWriter
doit êtreflush()
édité. En fait, cela nous montre une autre façon de contourner l'avertissement, car leBufferWriter
n'est jamais fermé dans les deux cas.la source
bw
va réellement écrire les données? Il est mis en mémoire tampon après tout, donc il suffit parfois d'écrire sur le disque (lorsque le tampon est plein et / ou activéflush()
etclose()
méthodes). Je suppose que laflush()
méthode devrait être appelée. Mais de toute façon, il n'est pas nécessaire d'utiliser le graveur en mémoire tampon lorsque nous l'écrivons immédiatement en un seul lot. Et si vous ne corrigez pas le code, vous pourriez vous retrouver avec des données écrites dans le fichier dans un ordre incorrect, voire même pas du tout écrites dans le fichier.flush()
est nécessaire d'être appelé, cela doit se produire chaque fois que l'appelant a décidé de fermer leFileWriter
. Cela devrait donc se produireprintToFile
juste avant toutreturn
s dans le bloc try. Cela ne doit pas faire partie dewriteFileWriter
et donc l'avertissement ne concerne rien à l'intérieur de cette fonction, mais l'appelant de cette fonction. Si nous avons une annotation,@LiftWarningToCaller("wanrningXXX")
cela aidera ce cas et similaire.