web-dev-qa-db-fra.com

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

La syntaxe Java 7 ) (également appelée bloc ARM ( La gestion automatique des ressources )) est agréable, bref et simple lorsque vous utilisez une seule ressource AutoCloseable. Cependant, je ne sais pas quel est le bon idiome lorsque je dois déclarer plusieurs ressources sont dépendants les uns des autres, par exemple un FileWriter et un BufferedWriter qui le recouvre. Bien entendu, cette question concerne tous les cas où certaines ressources AutoCloseable sont encapsulées, pas seulement celles-ci. deux classes spécifiques.

Je suis venu avec les trois alternatives suivantes:

1)

L'idiome naïf que j'ai vu consiste à 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 gentil et court, mais c'est cassé. Parce que le FileWriter sous-jacent n'est 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 close du wrapping BufferedWriter. Le problème est que, si une exception est levée du constructeur de bw, son close ne sera pas appelé et donc le FileWriter sous-jacent 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'habillage sont déclarées dans les variables gérées par ARM, elles seront donc toutes les deux fermées, mais le sous-jacent fw.close() sera appelé deux fois . : non seulement directement, mais aussi par l’emballage bw.close().

Cela ne devrait pas poser de 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 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 que close puisse être appelé plusieurs fois:

Notez que contrairement à la méthode de fermeture de Java.io.Closeable, cette méthode de fermeture n'a pas besoin d'être idempotente. En d'autres termes, l'appel de cette méthode rapprochée plus d'une fois peut avoir un effet secondaire visible, contrairement à Closeable.close qui doit ne pas avoir d'effet s'il est appelé plus d'une fois. Cependant, les implémenteurs de cette interface sont vivement 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 ne contient aucune ressource, il délègue uniquement des délégués au fw, il devrait donc suffire de fermer uniquement le fw sous-jacent.

D'autre part, la syntaxe est un peu irrégulière et Eclipse émet un avertissement, ce qui, à mon avis, est une fausse alerte, mais qui reste un avertissement auquel il faut s'attaquer:

Fuite de ressources: 'bw' n'est jamais fermé


Alors, quelle approche choisir? Ou ai-je oublié un autre langage qui est le correct ?

161
Natix

Voici mon point de vue sur les alternatives:

1)

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

Pour moi, la meilleure chose à faire à Java du C++ traditionnel il y a 15 ans était que vous pouviez faire confiance à votre programme. Même si les choses tournent mal et vont mal, ce qu'elles font souvent, je le souhaite. le reste du code doit afficher le meilleur comportement et l'odeur de roses. En effet, le BufferedWriter risque de lever une exception. Par exemple, manquer de mémoire ne serait pas inhabituel. Pour d'autres décorateurs, savez-vous lequel de Java.io Les classes wrappers jettent une exception vérifiée de leurs constructeurs? Je ne. La compréhension du code n’est pas très utile si vous vous basez sur ce type de connaissances obscures.

Il y a aussi la "destruction". S'il y a une condition d'erreur, vous ne voudrez probablement pas jeter les ordures dans un fichier qui doit être supprimé (code pour celui non affiché). Bien entendu, la suppression du fichier est également une autre opération intéressante à effectuer en tant que traitement des erreurs.

Généralement, vous voulez que les blocs finally soient aussi courts et fiables que possible. Ajouter des couleurs n'aide pas cet objectif. Pour de nombreuses versions, certaines des classes de mise en mémoire tampon du JDK avaient un bogue dans lequel une exception de flush au sein de close provoquait que close sur l'objet décoré ne soit pas appelé. Bien que cela soit corrigé depuis un certain temps, attendez-vous d'autres implémentations.

2)

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

Nous sommes toujours dans le bloc final implicite (maintenant avec répété close - la situation empire à mesure que vous ajoutez de nouveaux décorateurs), mais la construction est sûre et nous devons implicitement bloquer, donc même un échec flush n'empêche pas la libération des ressources.

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 installé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 qu'ils font de la compression et ont besoin d'écrire des bits pour finir, et ne peuvent pas tout vider.

Verdict

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

74

Le premier style est celui suggéré par Oracle . BufferedWriter ne lève pas les exceptions vérifiées. Par conséquent, si une exception est levée, le programme ne devrait pas en récupérer, ce qui rend la récupération des ressources essentiellement inutile.

Principalement parce que cela pouvait arriver dans un fil, le fil étant en train de mourir mais le programme continuait - par exemple, il y avait une panne de mémoire temporaire qui n'a pas été assez longue pour nuire gravement au reste du programme. Cependant, il s’agit plutôt d’un cas délicat, et si cela se produit assez souvent pour que la fuite de ressources devienne un problème, l’essai avec des ressources est le moindre de vos problèmes.

19
Daniel C. Sobral

Option 4

Modifiez vos ressources pour qu'elles soient Closeable, 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 deux fois la ressource. (C’était vrai avant ARM aussi.) Plus d’informations plus bas.

Option 5

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

Option 6

Ne pas utiliser ARM et avoir vos appels finalement proches () dans un try/catch eux-mêmes.

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

Dans tous ces exemples, les appels finalement close () devraient être dans un bloc catch. Oublié pour la lisibilité.

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

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 parce que fw pas fermé si exception lors de 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();
}
5
Jeanne Boyarsky

Pour accepter les commentaires précédents: le plus simple est (2) d'utiliser Closeable ressources et de les déclarer dans l'ordre dans la clause try-with-resources. Si vous n'avez que AutoCloseable, vous pouvez les envelopper dans une autre classe (imbriquée) qui vérifie simplement que close n'est appelé qu'une seule fois (motif de façade), 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 en cours de route.

Vous pouvez également créer manuellement une ressource chaînée à l'aide d'une méthode de fabrique statique. ceci encapsule la chaîne et gère le nettoyage s'il échoue en partie:

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 ressource unique dans une clause try-with-resources:

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

La complexité provient de la gestion de plusieurs exceptions. sinon c'est juste "des ressources proches que vous avez acquises jusqu'à présent". Une pratique courante semble être d’abord d’initialiser la variable qui contient l’objet qui contient la ressource sur null (ici fileWriter), 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 génériquement:

static <T extends AutoCloseable, U extends AutoCloseable, V>
    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 chaîner trois ressources, etc.

En mathématique, vous pouvez même enchaîner trois fois en enchaînant deux ressources à la fois. 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 (vous commencez donc par un [~ # ~] v [~ # ~] et se termine par un [~ # ~] s [~ # ~] , en appliquant [~ # ~] u [~ # ~] , [~ # ~] t [~ # ~] et [~ # ~] s [~ # ~] à leur tour ), vous obtenez le même si vous enchaînez d'abord [~ # ~] s [~ # ~] et [~ # ~] t [~ # ~] , puis [~ # ~] u [~ # ~] , correspondant à (ST) U , ou si vous avez déjà enchaîné [~ # ~] t [~ # ~] et [~ # ~] u [~ # ~] , puis [~ # ~] s [~ # ~] , correspondant à S (TU) . Cependant, il serait plus clair de simplement écrire une chaîne explicite à trois volets dans une seule fonction factory.

3
Nils von Barth

Je voulais juste m'appuyer sur la suggestion de Jeanne Boyarsky de ne pas utiliser ARM mais de m'assurer que le FileWriter est toujours fermé exactement une fois. Ne croyez pas qu'il y a un problème 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 n’est que du sucre syntaxique, nous ne pouvons pas toujours l’utiliser pour remplacer des blocs. Tout comme nous ne pouvons pas toujours utiliser une boucle for-each pour réaliser quelque chose qui est possible avec itérateurs.

3
AmyGamy

Puisque vos ressources sont imbriquées, 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
}
2
poison

Je dirais que n'utilisez pas ARM et continuez avec Closeable. Utilisez une 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 la fin de BufferedWriter car il ne s'agit pas simplement de déléguer la fin de FileWriter, mais il effectue un nettoyage comme flushBuffer.

0
sakthisundar

Ma solution consiste à effectuer une refactorisation de "méthode d'extraction", comme suit:

static AutoCloseable writeFileWriter(FileWriter fw, String 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(String 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(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}

Pour les concepteurs de bibliothèques de classes, je leur proposerai 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 à tirer est que l'ajout d'une nouvelle fonctionnalité peut impliquer l'ajout de nombreuses autres. Dans ce cas Java, évidemment ARM fonctionnera mieux avec un mécanisme de transfert de propriété de ressource.

MISE À JOUR

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

Comme suggéré par un commentaire, si flush() doit être appelé avant de fermer l'écrivain, nous devons le faire avant toute instruction return (implicite ou explicite) à l'intérieur du bloc try. Il n’existe actuellement aucun moyen de s’assurer que l’appelant fait cela, je pense, donc cela doit être documenté pour writeFileWriter.

PDATE AGAIN

La mise à jour ci-dessus rend @SuppressWarning Inutile car la fonction doit renvoyer 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 ce problème, nous avons besoin d’un AutoClosable personnalisé qui, chaque fois qu’il se ferme, le soulignement BufferedWriter doit être flush() ed. En fait, cela nous montre une autre façon de contourner l’avertissement, car le BufferWriter n’est jamais fermé de toute façon.

0
Earth Engine