Est-il nécessaire de fermer séparément chaque OutputStream et Writer imbriqués?

127

J'écris un morceau de code:

OutputStream outputStream = new FileOutputStream(createdFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(gzipOutputStream));

Dois-je fermer chaque flux ou écrivain comme suit?

gzipOutputStream.close();
bw.close();
outputStream.close();

Ou est-ce que la fermeture du dernier flux sera bien?

bw.close();
Adon Smith
la source
1
Pour la question obsolète Java 6 correspondante, voir stackoverflow.com/questions/884007/…
Raedwald
2
Notez que votre exemple a un bogue qui peut entraîner une perte de données, car vous fermez les flux pas dans l'ordre dans lequel vous les avez ouverts. Lors de la fermeture d'un, BufferedWriteril peut avoir besoin d'écrire des données tamponnées dans le flux sous-jacent, qui dans votre exemple est déjà fermé. Éviter ces problèmes est un autre avantage des approches d' essai avec les ressources présentées dans les réponses.
Joe23

Réponses:

150

En supposant que tous les flux sont créés correctement, oui, la simple fermeture bwconvient parfaitement à ces implémentations de flux ; mais c'est une grande hypothèse.

J'utiliserais try-with-resources ( tutoriel ) afin que tout problème de construction des flux suivants qui lancent des exceptions ne laisse pas les flux précédents en suspens, et vous n'avez donc pas à compter sur l'implémentation du flux ayant l'appel pour fermer le flux sous-jacent:

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

Notez que vous n'appelez plus closedu tout.

Remarque importante : pour que try-with-resources les ferme, vous devez affecter les flux aux variables lorsque vous les ouvrez, vous ne pouvez pas utiliser l'imbrication. Si vous utilisez l'imbrication, une exception lors de la construction de l'un des derniers flux (par exemple GZIPOutputStream) laissera ouvert tout flux construit par les appels imbriqués à l'intérieur. À partir de JLS §14.20.3 :

Une instruction try-with-resources est paramétrée avec des variables (appelées ressources) qui sont initialisées avant l'exécution du trybloc et fermées automatiquement, dans l'ordre inverse à partir duquel elles ont été initialisées, après l'exécution du trybloc.

Notez le mot «variables» (je souligne) .

Par exemple, ne faites pas ceci:

// DON'T DO THIS
try (BufferedWriter bw = new BufferedWriter(
        new OutputStreamWriter(
        new GZIPOutputStream(
        new FileOutputStream(createdFile))))) {
    // ...
}

... car une exception du GZIPOutputStream(OutputStream)constructeur (qui dit qu'il peut lancer IOExceptionet écrit un en-tête dans le flux sous-jacent) laisserait FileOutputStreamouvert. Étant donné que certaines ressources ont des constructeurs qui peuvent lancer et d'autres pas, c'est une bonne habitude de les énumérer séparément.

Nous pouvons revérifier notre interprétation de cette section JLS avec ce programme:

public class Example {

    private static class InnerMost implements AutoCloseable {
        public InnerMost() throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
        }
    }

    private static class Middle implements AutoCloseable {
        private AutoCloseable c;

        public Middle(AutoCloseable c) {
            System.out.println("Constructing " + this.getClass().getName());
            this.c = c;
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    private static class OuterMost implements AutoCloseable {
        private AutoCloseable c;

        public OuterMost(AutoCloseable c) throws Exception {
            System.out.println("Constructing " + this.getClass().getName());
            throw new Exception(this.getClass().getName() + " failed");
        }

        @Override
        public void close() throws Exception {
            System.out.println(this.getClass().getName() + " closed");
            c.close();
        }
    }

    public static final void main(String[] args) {
        // DON'T DO THIS
        try (OuterMost om = new OuterMost(
                new Middle(
                    new InnerMost()
                    )
                )
            ) {
            System.out.println("In try block");
        }
        catch (Exception e) {
            System.out.println("In catch block");
        }
        finally {
            System.out.println("In finally block");
        }
        System.out.println("At end of main");
    }
}

... qui a la sortie:

Exemple de construction $ InnerMost
Exemple de construction $ Middle
Exemple de construction $ OuterMost
Dans le bloc catch
Dans enfin bloc
À la fin de la main

Notez qu'il n'y a pas d'appels closelà-bas.

Si nous corrigeons main:

public static final void main(String[] args) {
    try (
        InnerMost im = new InnerMost();
        Middle m = new Middle(im);
        OuterMost om = new OuterMost(m)
        ) {
        System.out.println("In try block");
    }
    catch (Exception e) {
        System.out.println("In catch block");
    }
    finally {
        System.out.println("In finally block");
    }
    System.out.println("At end of main");
}

puis nous recevons les closeappels appropriés :

Exemple de construction $ InnerMost
Exemple de construction $ Middle
Exemple de construction $ OuterMost
Exemple $ Middle fermé
Exemple $ InnerMost fermé
Exemple $ InnerMost fermé
Dans le bloc catch
Dans enfin bloc
À la fin de la main

(Oui, deux appels à InnerMost#closeest correct; l'un provient de Middle, l'autre de try-with-resources.)

TJ Crowder
la source
7
+1 pour avoir noté que des exceptions peuvent être lancées lors de la construction des flux, bien que je note que de manière réaliste, vous allez soit obtenir une exception de mémoire insuffisante ou quelque chose d'aussi grave (à quel point cela n'a vraiment pas d'importance si vous fermez vos flux, car votre application est sur le point de se fermer), ou ce sera GZIPOutputStream qui lèvera une IOException; les autres constructeurs n'ont pas d'exceptions vérifiées, et il n'y a aucune autre circonstance susceptible de produire une exception d'exécution.
Jules
5
@Jules: Oui, pour ces flux spécifiques, en effet. Il s'agit plus de bonnes habitudes.
TJ Crowder
2
@PeterLawrey: Je ne suis pas du tout d'accord avec l'utilisation de mauvaises habitudes ou non en fonction de la mise en œuvre du flux. :-) Ce n'est pas une distinction YAGNI / no-YAGNI, il s'agit de modèles qui font un code fiable.
TJ Crowder
2
@PeterLawrey: Il n'y a rien au-dessus de ne pas faire confiance non java.ioplus. Certains flux - généralisation, certaines ressources - proviennent des constructeurs. Donc, s'assurer que plusieurs ressources sont ouvertes individuellement afin qu'elles puissent être fermées de manière fiable si une ressource suivante est lancée est juste une bonne habitude, à mon avis. Vous pouvez choisir de ne pas le faire si vous n'êtes pas d'accord, c'est très bien.
TJ Crowder le
2
@PeterLawrey: Vous préconisez donc de prendre le temps de regarder le code source d'une implémentation pour quelque chose qui documente une exception, au cas par cas, puis de dire "Oh, eh bien, ça ne lance pas vraiment, donc. .. "et enregistrer quelques caractères de frappe? Nous nous séparons là-bas, en grand. :-) De plus, j'ai juste regardé, et ce n'est pas théorique: GZIPOutputStreamle constructeur de s écrit un en-tête dans le flux. Et ainsi il peut jeter. Alors maintenant, la position est de savoir si je pense qu'il vaut la peine d' essayer de fermer le flux après avoir écrit jeté. Ouais: je l'ai ouvert, je devrais au moins essayer de le fermer.
TJ Crowder
12

Vous pouvez fermer le flux le plus externe, en fait vous n'avez pas besoin de conserver tous les flux encapsulés et vous pouvez utiliser Java 7 try-with-resources.

try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                     new GZIPOutputStream(new FileOutputStream(createdFile)))) {
     // write to the buffered writer
}

Si vous vous abonnez à YAGNI, ou si vous n'en avez pas besoin, vous ne devriez ajouter que le code dont vous avez réellement besoin. Vous ne devriez pas ajouter de code dont vous pensez avoir besoin, mais en réalité, cela ne fait rien d'utile.

Prenez cet exemple et imaginez ce qui pourrait mal tourner si vous ne le faisiez pas et quel en serait l'impact?

try (
    OutputStream outputStream = new FileOutputStream(createdFile);
    GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
    BufferedWriter bw = new BufferedWriter(osw)
    ) {
    // ...
}

Commençons par FileOutputStream qui appelle openà faire tout le vrai travail.

/**
 * Opens a file, with the specified name, for overwriting or appending.
 * @param name name of file to be opened
 * @param append whether the file is to be opened in append mode
 */
private native void open(String name, boolean append)
    throws FileNotFoundException;

Si le fichier n'est pas trouvé, il n'y a pas de ressource sous-jacente à fermer, donc le fermer ne fera aucune différence. Si le fichier existe, il doit lancer une exception FileNotFoundException. Il n'y a donc rien à gagner en essayant de fermer la ressource à partir de cette seule ligne.

La raison pour laquelle vous devez fermer le fichier est lorsque le fichier est ouvert avec succès, mais vous obtenez plus tard une erreur.

Regardons le prochain flux GZIPOutputStream

Il y a du code qui peut lever une exception

private void writeHeader() throws IOException {
    out.write(new byte[] {
                  (byte) GZIP_MAGIC,        // Magic number (short)
                  (byte)(GZIP_MAGIC >> 8),  // Magic number (short)
                  Deflater.DEFLATED,        // Compression method (CM)
                  0,                        // Flags (FLG)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Modification time MTIME (int)
                  0,                        // Extra flags (XFLG)
                  0                         // Operating system (OS)
              });
}

Cela écrit l'en-tête du fichier. Maintenant, il serait très inhabituel pour vous de pouvoir ouvrir un fichier pour l'écriture mais de ne pas pouvoir y écrire même 8 octets, mais imaginons que cela puisse arriver et nous ne fermons pas le fichier par la suite. Qu'arrive-t-il à un fichier s'il n'est pas fermé?

Vous n'obtenez pas d'écritures non vidées, elles sont supprimées et dans ce cas, il n'y a pas d'octets écrits avec succès dans le flux qui n'est de toute façon pas mis en mémoire tampon à ce stade. Mais un fichier qui n'est pas fermé ne vit pas éternellement, FileOutputStream a

protected void finalize() throws IOException {
    if (fd != null) {
        if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
            flush();
        } else {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }
}

Si vous ne fermez pas du tout un fichier, il se ferme de toute façon, mais pas immédiatement (et comme je l'ai dit, les données qui sont laissées dans un tampon seront perdues de cette façon, mais il n'y en a pas à ce stade)

Quelle est la conséquence de ne pas fermer le dossier immédiatement? Dans des conditions normales, vous risquez de perdre certaines données et vous risquez de manquer de descripteurs de fichiers. Mais si vous avez un système où vous pouvez créer des fichiers mais que vous ne pouvez rien y écrire, vous avez un problème plus grave. c'est-à-dire qu'il est difficile d'imaginer pourquoi vous essayez à plusieurs reprises de créer ce fichier malgré le fait que vous échouez.

OutputStreamWriter et BufferedWriter ne lancent pas d'exception IOException dans leurs constructeurs, de sorte que le problème qu'ils provoqueraient n'est pas clair. Dans le cas de BufferedWriter, vous pouvez obtenir une OutOfMemoryError. Dans ce cas, il déclenchera immédiatement un GC qui, comme nous l'avons vu, fermera quand même le fichier.

Peter Lawrey
la source
1
Voir la réponse de TJ Crowder pour les situations où cela pourrait échouer.
TimK
@TimK pouvez-vous fournir un exemple de l'endroit où le fichier est créé mais le flux échoue plus tard et quelle en est la conséquence. Le risque d'échec est extrêmement faible et l'impact est insignifiant. Pas besoin de rendre le plus compliqué qu'il ne doit l'être.
Peter Lawrey
1
GZIPOutputStream(OutputStream)documents IOExceptionet, en regardant la source, écrit en fait un en-tête. Ce n'est donc pas théorique, ce constructeur peut lancer. Vous pouvez penser qu'il est normal de laisser le sous-jacent FileOutputStreamouvert après avoir écrit dessus. Je ne.
TJ Crowder
1
@TJCrowder Je lève mon chapeau à quiconque est un développeur JavaScript professionnel expérimenté (et d'autres langages en plus). Je ne pouvais pas le faire. ;)
Peter Lawrey
1
Juste pour revenir sur cela, l'autre problème est que si vous utilisez un GZIPOutputStream sur un fichier et que vous n'appelez pas de manière explicite, il sera appelé dans sa mise en œuvre proche. Ce n'est pas dans un essai ... enfin, si la fin / le vidage lève une exception, le descripteur de fichier sous-jacent ne sera jamais fermé.
robert_difalco
6

Si tous les flux ont été instanciés, la fermeture uniquement du flux externe est très bien.

La documentation sur l' Closeableinterface indique que la méthode close:

Ferme ce flux et libère toutes les ressources système qui lui sont associées.

Les ressources du système de libération comprennent des flux de fermeture.

Il déclare également que:

Si le flux est déjà fermé, l'invocation de cette méthode n'a aucun effet.

Donc, si vous les fermez explicitement par la suite, il ne se passera rien de mal.

Grzegorz Żur
la source
2
Cela suppose qu'il n'y a pas d'erreurs lors de la construction des flux, ce qui peut être vrai ou non pour ceux répertoriés, mais ce n'est pas vrai de manière fiable en général.
TJ Crowder
6

Je préfère utiliser la try(...)syntaxe (Java 7), par exemple

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Dmitry Bychenko
la source
4
Bien que je sois d'accord avec vous, vous voudrez peut-être souligner l'avantage de cette approche et répondre au point si le PO doit fermer les flux enfants / internes
MadProgrammer
5

Ce sera bien si vous ne fermez que le dernier flux - l'appel de fermeture sera également envoyé aux flux sous-jacents.

Codeversum
la source
1
Voir le commentaire sur la réponse de Grzegorz Żur.
TJ Crowder le
5

Non, le niveau le plus élevé Streamou readergarantira que tous les flux / lecteurs sous-jacents sont fermés.

Vérifiez la mise en œuvre de la close()méthode de votre flux de niveau supérieur.

TheLostMind
la source
5

Dans Java 7, il existe une fonctionnalité try-with-resources . Vous n'avez pas besoin de fermer explicitement vos flux, il s'en chargera.

Sivakumar
la source