¿Idioma correcto para gestionar múltiples recursos encadenados en el bloque try-with-resources?

168

La sintaxis de prueba con recursos de Java 7 (también conocida como bloque ARM ( Gestión automática de recursos )) es agradable, corta y directa cuando se usa solo un AutoCloseablerecurso. Sin embargo, no estoy seguro de lo que es el idioma correcto cuando necesito para declarar múltiples recursos que son dependientes entre sí, por ejemplo, un FileWritery una BufferedWriterque lo envuelve. Por supuesto, esta pregunta se refiere a cualquier caso cuando AutoCloseablese envuelven algunos recursos, no solo estas dos clases específicas.

Se me ocurrieron las tres alternativas siguientes:

1)

El lenguaje ingenuo que he visto es declarar solo el contenedor de nivel superior en la variable administrada por ARM:

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

Esto es bueno y corto, pero está roto. Debido a que el subyacente FileWriterno se declara en una variable, nunca se cerrará directamente en el finallybloque generado . Se cerrará solo a través del closemétodo de envoltura BufferedWriter. El problema es que si se lanza una excepción desde el bwconstructor, closeno se llamará y, por lo tanto, el subyacente FileWriter no se cerrará .

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

Aquí, tanto el recurso subyacente como el envoltorio se declaran en las variables administradas por ARM, por lo que ambos se cerrarán, pero el subyacente fw.close() se llamará dos veces : no solo directamente, sino también a través del ajuste bw.close().

Esto no debería ser un problema para estas dos clases específicas que ambas implementan Closeable(que es un subtipo de AutoCloseable), cuyo contrato establece que closese permiten múltiples llamadas a :

Cierra esta secuencia y libera todos los recursos del sistema asociados a ella. Si la secuencia ya está cerrada, invocar este método no tiene ningún efecto.

Sin embargo, en un caso general, puedo tener recursos que se implementan solo AutoCloseable(y no Closeable), lo que no garantiza que closese pueda llamar varias veces:

Tenga en cuenta que, a diferencia del método de cierre de java.io.Closeable, no es necesario que este método de cierre sea idempotente. En otras palabras, llamar a este método de cierre más de una vez puede tener algún efecto secundario visible, a diferencia de Closeable.close, que se requiere que no tenga efecto si se llama más de una vez. Sin embargo, se recomienda encarecidamente a los implementadores de esta interfaz que hagan idempotentes sus métodos cercanos.

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

Esta versión debería ser teóricamente correcta, porque solo fwrepresenta un recurso real que necesita ser limpiado. El bwmismo no contiene ningún recurso, solo delega en el fw, por lo que debería ser suficiente para cerrar solo el subyacente fw.

Por otro lado, la sintaxis es un poco irregular y, además, Eclipse emite una advertencia, que creo que es una falsa alarma, pero sigue siendo una advertencia con la que uno tiene que lidiar:

Fuga de recursos: 'bw' nunca se cierra


Entonces, ¿qué enfoque elegir? ¿O me he perdido algún otro idioma que es el correcto ?

Natix
fuente
44
Por supuesto, si el constructor de FileWriter subyacente arroja una excepción, ni siquiera se abre y todo está bien. De lo que se trata el primer ejemplo es de qué sucede si se crea FileWriter, pero el constructor de BufferedWriter arroja una excepción.
Natix
66
Vale la pena señalar que BufferedWriter no lanzará una excepción. ¿Hay algún ejemplo en el que puedas pensar si esta pregunta no es puramente académica?
Peter Lawrey el
10
@PeterLawrey Sí, tiene razón en que el constructor de BufferedWriter en este escenario probablemente no arroje una excepción, pero como he señalado, esta pregunta se refiere a cualquier recurso de estilo decorador. Pero por ejemplo public BufferedWriter(Writer out, int sz)puede lanzar un IllegalArgumentException. Además, puedo extender BufferedWriter con una clase que arrojaría algo de su constructor o crearía cualquier contenedor personalizado que necesite.
Natix
55
El BufferedWriterconstructor puede lanzar fácilmente una excepción. OutOfMemoryErrores probablemente el más común, ya que asigna una buena cantidad de memoria para el búfer (aunque puede indicar que desea reiniciar todo el proceso). / Necesitas flushtu BufferedWritersi no cierras y quieres conservar el contenido (generalmente solo el caso sin excepción). FileWriterrecoge lo que sea que sea la codificación de archivo "predeterminada"; es mejor ser explícito.
Tom Hawtin - tackline
10
@Natix Deseo que todas las preguntas en SO estén tan bien investigadas y claras como esta. Desearía poder votar esto 100 veces.
Geek

Respuestas:

75

Aquí está mi opinión sobre las alternativas:

1)

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

Para mí, lo mejor que llegó a Java desde C ++ tradicional hace 15 años fue que podía confiar en su programa. Incluso si las cosas están mal y van mal, lo que a menudo hacen, quiero que el resto del código tenga el mejor comportamiento y el olor a rosas. De hecho, el BufferedWriterpodría arrojar una excepción aquí. Quedarse sin memoria no sería inusual, por ejemplo. Para otros decoradores, ¿sabe cuál de las java.ioclases de envoltura arroja una excepción marcada de sus constructores? Yo no. La comprensión del código no es muy buena si confía en ese tipo de conocimiento oscuro.

También está la "destrucción". Si hay una condición de error, entonces probablemente no desee tirar basura a un archivo que necesita eliminarse (el código para eso no se muestra). Aunque, por supuesto, eliminar el archivo también es otra operación interesante para manejar errores.

En general, desea que los finallybloques sean lo más cortos y confiables posible. Agregar rubores no ayuda a este objetivo. Para muchas versiones, algunas de las clases de almacenamiento en búfer en el JDK tenían un error en el que no se llamaba una excepción flushinterna closecausada closeen el objeto decorado. Si bien eso se ha solucionado durante algún tiempo, espere de otras implementaciones.

2)

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

Todavía nos estamos volcando en el bloque implícito finalmente (ahora con repetición close, esto empeora a medida que agrega más decoradores), pero la construcción es segura y tenemos que bloquear implícitamente finalmente para que incluso un error flushno impida la liberación de recursos.

3)

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

Hay un error aquí. Debiera ser:

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

Algunos decoradores mal implementados son de hecho recursos y deberán cerrarse de manera confiable. Además, es posible que algunas secuencias deban cerrarse de una manera particular (tal vez están haciendo compresión y necesitan escribir bits para terminar, y no pueden simplemente vaciar todo.

Veredicto

Aunque 3 es una solución técnicamente superior, las razones de desarrollo de software hacen de 2 la mejor opción. Sin embargo, probar con recursos sigue siendo una solución inadecuada y debe seguir con el lenguaje Execute Around , que debería tener una sintaxis más clara con los cierres en Java SE 8.

Tom Hawtin - tackline
fuente
44
En la versión 3, ¿cómo sabe que bw no requiere que se llame a su cierre? E incluso si puede estar seguro, ¿no es eso también "conocimiento oscuro" como usted menciona para la versión 1?
TimK
3
"Las razones de desarrollo de software hacen que 2 sea la mejor opción " ¿Puede explicar esta afirmación con más detalle?
Duncan Jones el
8
¿Puede dar un ejemplo de la "expresión idiomática Ejecutar alrededor con cierres"
Markus
2
¿Puede explicar qué es "una sintaxis más clara con cierres en Java SE 8"?
petertc
1
El ejemplo de "Ejecutar alrededor del idioma" está aquí: stackoverflow.com/a/342016/258772
mrts
20

El primer estilo es el sugerido por Oracle . BufferedWriterno arroja excepciones comprobadas, por lo que si se lanza alguna excepción, no se espera que el programa se recupere, lo que hace que la recuperación de recursos sea principalmente discutible.

Principalmente porque podría suceder en un subproceso, con la interrupción del subproceso pero el programa aún continúa; por ejemplo, hubo una interrupción temporal de la memoria que no fue lo suficientemente larga como para perjudicar gravemente al resto del programa. Sin embargo, es un caso bastante arrinconado, y si sucede con la frecuencia suficiente como para hacer que la fuga de recursos sea un problema, la prueba con recursos es el menor de sus problemas.

Daniel C. Sobral
fuente
2
También es el enfoque recomendado en la tercera edición de Effective Java.
shmosel
5

Opcion 4

Cambie sus recursos para que se puedan cerrar, no se puedan cerrar automáticamente si puede. El hecho de que los constructores se puedan encadenar implica que no es extraño cerrar el recurso dos veces. (Esto era cierto antes de ARM también.) Más sobre esto a continuación.

Opción 5

¡No use ARM y código con mucho cuidado para garantizar que close () no se llame dos veces!

Opcion 6

No uses ARM y ten tus llamadas finalmente close () en un intento / captura ellos mismos.

Por qué no creo que este problema sea exclusivo de ARM

En todos estos ejemplos, las llamadas finalmente close () deben estar en un bloque catch. Dejado por legibilidad.

No es bueno porque fw se puede cerrar dos veces. (que está bien para FileWriter pero no en su ejemplo hipotético):

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

No es bueno porque fw no se cierra si es una excepción al construir un BufferedWriter. (de nuevo, no puede suceder, pero en su ejemplo hipotético):

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

Solo quería basarme en la sugerencia de Jeanne Boyarsky de no usar ARM pero asegurarme de que FileWriter siempre esté cerrado exactamente una vez. No pienses que hay problemas aquí ...

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

Supongo que dado que ARM es solo azúcar sintáctico, no siempre podemos usarlo para reemplazar finalmente los bloques. Al igual que no siempre podemos usar un ciclo for-each para hacer algo que sea posible con iteradores.

AmyGamy
fuente
55
Si tanto tu trycomo los finallybloques arrojan excepciones, esta construcción perderá la primera (y potencialmente más útil).
rxg
3

Para coincidir con los comentarios anteriores: lo más simple es (2) usar Closeablerecursos y declararlos en orden en la cláusula try-with-resources. Si solo tiene AutoCloseable, puede envolverlos en otra clase (anidada) que solo verifica que closesolo se llama una vez (Patrón de fachada), por ejemplo, al tener private bool isClosed;. En la práctica, incluso Oracle solo (1) encadena los constructores y no maneja correctamente las excepciones en la mitad de la cadena.

Alternativamente, puede crear manualmente un recurso encadenado, utilizando un método de fábrica estático; esto encapsula la cadena y maneja la limpieza si falla parcialmente:

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

Luego puede usarlo como un recurso único en una cláusula de prueba con recursos:

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

La complejidad proviene de manejar múltiples excepciones; de lo contrario, son solo "recursos cercanos que has adquirido hasta ahora". Una práctica común parece ser inicializar primero la variable que contiene el objeto que contiene el recurso null(aquí fileWriter), y luego incluir una comprobación nula en la limpieza, pero eso parece innecesario: si el constructor falla, no hay nada que limpiar, así que podemos dejar que se propague esa excepción, lo que simplifica un poco el código.

Probablemente podrías hacer esto genéricamente:

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

Del mismo modo, puede encadenar tres recursos, etc.

Como matemático aparte, incluso podría encadenar tres veces encadenando dos recursos a la vez, y sería asociativo, lo que significa que obtendría el mismo objeto en caso de éxito (porque los constructores son asociativos), y las mismas excepciones si hubiera un fallo en cualquiera de los constructores. Suponiendo que agregó una S a la cadena anterior (por lo que comienza con una V y termina con una S , aplicando U , T y S a su vez), obtiene lo mismo si primero encadena S y T , luego U , correspondiente a (ST) U , o si primero encadenó T y U , entoncesS , correspondiente a S (TU) . Sin embargo, sería más claro simplemente escribir una cadena triple explícita en una sola función de fábrica.

Nils von Barth
fuente
¿Estoy reuniendo correctamente que uno todavía necesita usar try-with-resource, como en try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE
@ErikE Sí, aún necesitaría usar try-with-resources, pero solo necesitaría usar una única función para el recurso encadenado: la función de fábrica encapsula el encadenamiento. He agregado un ejemplo de uso; ¡Gracias!
Nils von Barth el
2

Como sus recursos están anidados, sus cláusulas de prueba también deberían ser:

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
}
veneno
fuente
55
Esto es bastante similar a mi segundo ejemplo. Si no se produce una excepción, se llamará a FileWriter closedos veces.
Natix
0

Yo diría que no use ARM y continúe con Closeable. Use un método como,

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

}

También debe considerar llamar cerca BufferedWriterya que no solo delega el cierre FileWriter, sino que también realiza algunas tareas de limpieza flushBuffer.

sakthisundar
fuente
0

Mi solución es hacer una refactorización de "método de extracción", de la siguiente manera:

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 se puede escribir

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

o

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

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

Para los diseñadores de la clase lib, les sugeriré que amplíen la AutoClosableinterfaz con un método adicional para suprimir el cierre. En este caso, podemos controlar manualmente el comportamiento cercano.

Para los diseñadores de idiomas, la lección es que agregar una nueva característica podría significar agregar muchas otras. En este caso de Java, obviamente la función ARM funcionará mejor con un mecanismo de transferencia de propiedad de recursos.

ACTUALIZAR

Originalmente, el código anterior requiere @SuppressWarningya que el BufferedWriterinterior de la función lo requiere close().

Como sugiere un comentario, si se flush()debe llamar antes de cerrar el escritor, debemos hacerlo antes de cualquier returndeclaración (implícita o explícita) dentro del bloque try. Actualmente no hay forma de garantizar que la persona que llama haga esto, creo, por lo que debe documentarse writeFileWriter.

ACTUALIZAR DE NUEVO

La actualización anterior hace @SuppressWarninginnecesaria, ya que requiere que la función devuelva el recurso a la persona que llama, por lo que no es necesario cerrarla. Desafortunadamente, esto nos lleva de vuelta al comienzo de la situación: la advertencia ahora se mueve nuevamente al lado de la persona que llama.

Así que para resolver adecuadamente esto, necesitamos un personalizarse AutoClosableque cada vez que se cierra, el subrayado BufferedWriterserá flush()ed. En realidad, esto nos muestra otra forma de evitar la advertencia, ya BufferWriterque nunca se cierra de ninguna manera.

Motor de tierra
fuente
La advertencia tiene su significado: ¿Podemos estar seguros aquí de que bwrealmente escribirá los datos? Se está amortiguada después de todo, por lo que sólo tiene que escribir en el disco a veces (cuando el búfer está lleno y / o sobre flush()y close()métodos). Supongo que el flush()método debería llamarse. Pero, de todos modos, no hay necesidad de usar el escritor almacenado en búfer, cuando lo escribimos inmediatamente en un lote. Y si no arregla el código, podría terminar con los datos escritos en el archivo en un orden incorrecto, o incluso no escritos en el archivo.
Petr Janeček
Si flush()es necesario llamar, sucederá cada vez que la persona que llama decida cerrar el FileWriter. Entonces, esto debería suceder printToFilejusto antes de cualquier returns en el bloque try. Esto no formará parte writeFileWritery, por lo tanto, la advertencia no se trata de nada dentro de esa función, sino del llamador de esa función. Si tenemos una anotación @LiftWarningToCaller("wanrningXXX")ayudará a este caso y similares.
Earth Engine