¿Es necesario cerrar cada OutputStream y Writer anidados por separado?

127

Estoy escribiendo un código:

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

¿Necesito cerrar cada transmisión o escritor como el siguiente?

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

¿O solo estará bien cerrar la última transmisión?

bw.close();
Adon Smith
fuente
1
Para la pregunta obsoleta correspondiente de Java 6, consulte stackoverflow.com/questions/884007/…
Raedwald
2
Tenga en cuenta que su ejemplo tiene un error que puede causar la pérdida de datos, porque está cerrando las transmisiones no en el orden en que las abrió. Al cerrar un archivo, BufferedWriteres posible que deba escribir datos almacenados en la secuencia subyacente, que en su ejemplo ya está cerrada. Evitar estos problemas es otra ventaja de los enfoques de prueba con recursos que se muestran en las respuestas.
Joe23

Respuestas:

150

Asumiendo que todas las transmisiones se crean bien, sí, solo cerrar bwestá bien con esas implementaciones de transmisiones ; Pero esa es una gran suposición.

Usaría try-with-resources ( tutorial ) para que cualquier problema que construya las secuencias posteriores que arroje excepciones no deje las secuencias anteriores suspendidas, por lo que no tiene que confiar en que la implementación de la secuencia tenga la llamada para cerrar la corriente subyacente:

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

Tenga en cuenta que ya no llama closeen absoluto.

Nota importante : para que los recursos de prueba con los cierren, debe asignar las secuencias a las variables a medida que las abre, no puede usar el anidamiento. Si usa la anidación, una excepción durante la construcción de una de las secuencias posteriores (por ejemplo, GZIPOutputStream) dejará abierta cualquier secuencia construida por las llamadas anidadas dentro de ella. De JLS §14.20.3 :

Una declaración de prueba con recursos se parametriza con variables (conocidas como recursos) que se inicializan antes de la ejecución del trybloque y se cierran automáticamente, en el orden inverso desde el que se inicializaron, después de la ejecución del trybloque.

Tenga en cuenta la palabra "variables" (mi énfasis) .

Por ejemplo, no hagas esto:

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

... porque una excepción del GZIPOutputStream(OutputStream)constructor (que dice que puede lanzar IOExceptiony escribe un encabezado en la secuencia subyacente) dejaría FileOutputStreamabierto. Dado que algunos recursos tienen constructores que pueden arrojar y otros no, es una buena costumbre enumerarlos por separado.

Podemos verificar nuestra interpretación de esa sección JLS con este programa:

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");
    }
}

... que tiene la salida:

Ejemplo de construcción $ InnerMost
Ejemplo de construcción $ Middle
Ejemplo de construcción $ OuterMost
En bloque de captura
En finalmente bloque
Al final de main

Tenga en cuenta que no hay llamadas closeallí.

Si arreglamos 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");
}

entonces recibimos las closellamadas apropiadas :

Ejemplo de construcción $ InnerMost
Ejemplo de construcción $ Middle
Ejemplo de construcción $ OuterMost
Ejemplo $ Medio cerrado
Ejemplo $ InnerMost cerrado
Ejemplo $ InnerMost cerrado
En bloque de captura
En finalmente bloque
Al final de main

(Sí, dos llamadas a InnerMost#closees correcta; una es de Middle, la otra es de prueba con recursos).

TJ Crowder
fuente
77
+1 por notar que pueden producirse excepciones durante la construcción de las transmisiones, aunque notaré que, de manera realista, obtendrá una excepción de falta de memoria o algo igualmente grave (en ese momento, realmente no importa si cierra sus transmisiones, porque su aplicación está a punto de salir), o será GZIPOutputStream el que arroje una IOException; el resto de los constructores no tienen excepciones comprobadas, y no hay otras circunstancias que puedan producir una excepción en tiempo de ejecución.
Jules
55
@Jules: Sí, para estas transmisiones específicas, de hecho. Se trata más de buenos hábitos.
TJ Crowder
2
@ PeterLawrey: Estoy totalmente en desacuerdo con usar malos hábitos o no, dependiendo de la implementación de la transmisión. :-) Esto no es una distinción YAGNI / no-YAGNI, se trata de patrones que crean un código confiable.
TJ Crowder
2
@PeterLawrey: No hay nada más sobre no confiar java.io, tampoco. Algunas corrientes, generalizando, algunos recursos , se lanzan desde los constructores. Por lo tanto, en mi opinión, asegurarse de que múltiples recursos se abran individualmente para que puedan cerrarse de manera confiable si un recurso posterior es solo un buen hábito. Puedes elegir no hacerlo si no estás de acuerdo, está bien.
TJ Crowder
2
@PeterLawrey: Por lo tanto, aboga por tomarse el tiempo para mirar el código fuente de una implementación para algo que documente una excepción, caso por caso, y luego decir "Oh, bueno, en realidad no arroja, entonces. .. "y guardar algunos caracteres de mecanografía? Nos separamos allí, a lo grande. :-) Además, acabo de mirar, y esto no es teórico: GZIPOutputStreamel constructor escribe un encabezado en la secuencia. Y para que pueda tirar. Así que ahora la posición es si creo que vale la pena molestarse en tratar de cerrar la transmisión después de escribir tiró. Sí: lo abrí, al menos debería intentar cerrarlo.
TJ Crowder
12

Puede cerrar la secuencia más externa, de hecho, no necesita retener todas las secuencias envueltas y puede usar Java 7 try-with-resources.

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

Si se suscribe a YAGNI, o no lo va a necesitar, solo debe agregar el código que realmente necesita. No debería agregar código que imagina que podría necesitar, pero en realidad no hace nada útil.

Tome este ejemplo e imagine qué podría salir mal si no hiciera esto y cuál sería el impacto.

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

Comencemos con FileOutputStream que llama opena hacer todo el trabajo real.

/**
 * 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 no se encuentra el archivo, no hay ningún recurso subyacente para cerrar, por lo que cerrarlo no hará ninguna diferencia. Si el archivo existe, debería lanzar una FileNotFoundException. Por lo tanto, no hay nada que ganar intentando cerrar el recurso solo desde esta línea.

La razón por la que necesita cerrar el archivo es cuando el archivo se abre con éxito, pero luego aparece un error.

Veamos la próxima transmisión GZIPOutputStream

Hay un código que puede lanzar una excepción

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)
              });
}

Esto escribe el encabezado del archivo. Ahora sería muy inusual para usted poder abrir un archivo para escribir pero no poder escribir incluso 8 bytes, pero imaginemos que esto podría suceder y no cerramos el archivo después. ¿Qué le sucede a un archivo si no está cerrado?

No se obtienen escrituras sin vaciar, se descartan y, en este caso, no hay bytes escritos correctamente en la secuencia que, de todos modos, no están almacenados en este momento. Pero un archivo que no está cerrado no vive para siempre, en cambio FileOutputStream tiene

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 no cierra un archivo, se cierra de todos modos, solo que no inmediatamente (y como dije, los datos que quedan en un búfer se perderán de esta manera, pero no hay ninguno en este momento)

¿Cuál es la consecuencia de no cerrar el archivo de inmediato? En condiciones normales, puede perder algunos datos y quedarse sin descriptores de archivo. Pero si tiene un sistema donde puede crear archivos pero no puede escribirles nada, tiene un problema mayor. es decir, es difícil imaginar por qué intenta repetidamente crear este archivo a pesar del hecho de que está fallando.

Tanto OutputStreamWriter como BufferedWriter no lanzan IOException en sus constructores, por lo que no está claro qué problema causarían. En el caso de BufferedWriter, puede obtener un OutOfMemoryError. En este caso, activará inmediatamente un GC, que como hemos visto cerrará el archivo de todos modos.

Peter Lawrey
fuente
1
Vea la respuesta de TJ Crowder para situaciones en las que esto podría fallar.
TimK
@TimK puede proporcionar un ejemplo de dónde se crea el archivo, pero la secuencia más tarde falla y cuál es la consecuencia. El riesgo de falla es extremadamente bajo y el impacto es trivial. No es necesario hacer lo más complicado de lo que debe ser.
Peter Lawrey
1
GZIPOutputStream(OutputStream)documentos IOExceptiony, mirando la fuente, en realidad escribe un encabezado. Entonces no es teórico, ese constructor puede tirar. Puede sentir que está bien dejar el subyacente FileOutputStreamabierto después de escribirle. Yo no.
TJ Crowder
1
@TJCrowder Cualquier persona que sea un desarrollador de JavaScript profesional experimentado (y otros idiomas además) me quito el sombrero. No pude hacerlo. ;)
Peter Lawrey
1
Solo para volver a visitar esto, el otro problema es que si está utilizando un GZIPOutputStream en un archivo y no llama a finalizar explícitamente, se lo llamará en su implementación cercana. Esto no está en un intento ... finalmente, si el acabado / vaciado arroja una excepción, el identificador de archivo subyacente nunca se cerrará.
robert_difalco
6

Si se han instanciado todos los flujos, entonces cerrar solo el más externo está bien.

La documentación en la Closeableinterfaz indica que el método de cierre:

Cierra esta secuencia y libera todos los recursos del sistema asociados a ella.

Los recursos del sistema de liberación incluyen el flujo de cierre.

También establece que:

Si la secuencia ya está cerrada, invocar este método no tiene ningún efecto.

Entonces, si los cierra explícitamente después, no pasará nada malo.

Grzegorz Żur
fuente
2
Esto supone que no hay errores en la construcción de las secuencias, lo que puede ser cierto o no para los listados, pero en general no es confiable.
TJ Crowder
6

Prefiero usar la try(...)sintaxis (Java 7), por ejemplo

try (OutputStream outputStream = new FileOutputStream(createdFile)) {
      ...
}
Dmitry Bychenko
fuente
44
Si bien estoy de acuerdo con usted, es posible que desee resaltar el beneficio de este enfoque y responder al punto si el OP necesita cerrar las secuencias secundarias / internas
MadProgrammer
5

Estará bien si solo cierra la última secuencia; la llamada de cierre también se enviará a las secuencias subyacentes.

Codeversum
fuente
1
Ver comentario sobre la respuesta de Grzegorz Żur.
TJ Crowder
5

No, el nivel superior Streamo readergarantizará que todas las transmisiones / lectores subyacentes estén cerradas.

Verifique la implementación del close()método de su flujo de nivel superior.

TheLostMind
fuente
5

En Java 7, hay una función de prueba con recursos . No necesita cerrar explícitamente sus transmisiones, se encargará de eso.

Sivakumar
fuente