with try the resource que examples ejemplo catch java try-with-resources

java - the - ¿Corregir expresiones idiomáticas para administrar múltiples recursos encadenados en el bloque try-with-resources?



try with resources examples (8)

La sintaxis Try-with-resources de Java 7 (también conocida como bloque ARM ( Automatic Resource Management )) es agradable, breve y directa cuando se usa solo un recurso de AutoCloseable . Sin embargo, no estoy seguro de cuál es la expresión correcta cuando necesito declarar múltiples recursos que dependen el uno del otro, por ejemplo, un FileWriter y un BufferedWriter que lo envuelve. Por supuesto, esta pregunta se refiere a cualquier caso cuando se envuelven algunos recursos de AutoCloseable , no solo estas dos clases específicas.

Se me ocurrieron las tres siguientes alternativas:

1)

La expresión ingenua 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. Como el FileWriter subyacente no se declara en una variable, nunca se cerrará directamente en el bloque finally generado. Se cerrará solo a través del método de close del BufferedWriter envoltura. El problema es que si se lanza una excepción desde el constructor de bw , no se llamará a su close y, por lo tanto , no se cerrará el FileWriter subyacente.

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 subyacente como el recurso de envoltura se declaran en las variables administradas por ARM, por lo que ambos se cerrarán, pero el fw.close() subyacente se llamará dos veces : no solo directamente, sino también a través del bw.close() .

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

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

Sin embargo, en un caso general, puedo tener recursos que implementan solo AutoCloseable (y no se pueden close ), lo que no garantiza que se pueda llamar al close 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 idempoten 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 debe ser teóricamente correcta, ya que solo fw representa un recurso real que debe limpiarse. El bw no contiene ningún recurso, solo delega al fw , por lo que debería ser suficiente para cerrar solo el fw subyacente.

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 hay que lidiar:

Fuga de recursos: ''bw'' nunca se cierra

Entonces, ¿qué enfoque elegir? ¿O me he perdido alguna otra expresión que es la correcta ?


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 se puede obtener de C ++ tradicional hace 15 años en Java es que puedes confiar en tu programa. Incluso si las cosas están en la basura y van mal, lo que a menudo hacen, quiero que el resto del código tenga el mejor comportamiento y huele a rosas. De hecho, el BufferedWriter podría arrojar una excepción aquí. Quedarse sin memoria no sería inusual, por ejemplo. Para otros decoradores, ¿sabe cuál de las clases de envoltura de java.io arroja una excepción marcada de sus constructores? Yo no. No codifica la comprensibilidad demasiado bien si confías 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 eliminar (código que no se muestra). Aunque, por supuesto, eliminar el archivo también es otra operación interesante para hacer como manejo de errores.

Generalmente, usted quiere que finally bloques sean lo más cortos y confiables posible. Agregar descargas 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 a una excepción de flush dentro de close causada close del objeto decorado. Si bien eso se ha solucionado por algún tiempo, lo esperan de otras implementaciones.

2)

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

Todavía estamos sonrojándonos en el bloque implícito finalmente (ahora con close repetido, esto empeora a medida que agrega más decoradores), pero la construcción es segura y tenemos que bloquear finalmente implícitamente, por lo que incluso una flush fallida no evita 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. También es posible que algunas secuencias deban cerrarse de una manera particular (tal vez estén haciendo compresión y necesiten escribir bits para finalizar, y no puedan simplemente eliminar todo.

Veredicto

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


Dado que sus recursos están anidados, sus cláusulas try-with 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 }


El primer estilo es el sugerido por Oracle . BufferedWriter no arroja excepciones comprobadas, por lo que si se lanza alguna excepción, no se espera que el programa se recupere de ella, lo que hace que la recuperación de los recursos sea en su mayoría discutible.

Principalmente porque podría suceder en un hilo, con el hilo muriendo 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 seriamente el resto del programa. Sin embargo, es un caso más bien de esquina, y si ocurre con la suficiente frecuencia como para hacer que la fuga de recursos sea un problema, el try-with-resources es el menor de tus problemas.


Mi solución es hacer una refactorización de "método de extracción", como sigue:

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 clase lib, les sugiero que extiendan la interfaz AutoClosable con un método adicional para suprimir el cierre. En este caso, podemos controlar manualmente el comportamiento de cierre.

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 característica ARM funcionará mejor con un mecanismo de transferencia de propiedad de los recursos.

ACTUALIZAR

Originalmente, el código anterior requiere @SuppressWarning ya que BufferedWriter dentro de la función requiere close() .

Como sugiere un comentario, si flush() debe llamarse antes de cerrar el escritor, debemos hacerlo antes de cualquier return (implícita o explícita) dentro del bloque try. Actualmente no hay forma de garantizar que la persona que llama lo haga, así que debe estar documentado para writeFileWriter .

ACTUALIZAR DE NUEVO

La actualización anterior hace que @SuppressWarning sea ​​innecesario ya que requiere que la función devuelva el recurso a la persona que llama, por lo que no es necesario que se cierre. Desafortunadamente, esto nos lleva de vuelta al principio de la situación: ahora la advertencia vuelve al lado de la persona que llama.

Entonces, para resolver esto correctamente, necesitamos un AutoClosable personalizado que cada vez que se cierra, el BufferedWriter subrayado debe estar flush() ed. En realidad, esto nos muestra otra forma de eludir la advertencia, ya que el BufferWriter nunca se cierra de ninguna manera.


Para coincidir con los comentarios anteriores: lo más simple es (2) utilizar los recursos de Closeable y declararlos en orden en la cláusula try-with-resources. Si solo tiene AutoCloseable , puede envolverlos en otra clase (anidada) que simplemente verifique que el close solo se llame una vez (Facade Pattern), por ejemplo, teniendo private bool isClosed; . En la práctica, incluso Oracle solo (1) encadena a los constructores y no maneja correctamente las excepciones a 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 en parte:

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 único recurso en una cláusula try-with-resources:

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

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

Probablemente puedas 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 un aspecto matemático, 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 una falla en cualquiera de los constructores. Asumiendo que agregaste una S a la cadena de arriba (para que comiences con una V y acabes con una S , aplicando U , T y S por turno), obtienes lo mismo si primero encadenas S y T , luego U , correspondiente a (ST) U , o si primero encadenaste T y U , entonces S , 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.


Solo quería basarme en la sugerencia de Jeanne Boyarsky de no usar ARM, pero asegurándome de que el FileWriter esté siempre cerrado exactamente una vez. No creo que haya ningún problema 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 bucle for-each para hacer algo que es posible con los iteradores.


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 debería considerar llamar a close de BufferedWriter ya que no solo está delegando el cierre de FileWriter , sino que hace una limpieza como flushBuffer .


Opción 4

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

Opción 5

¡No use ARM y código muy cuidadosamente para asegurarse de que close () no se llame dos veces!

Opción 6

No use ARM y tenga sus llamadas close () finalmente en try / catch.

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

En todos estos ejemplos, las llamadas close () finalmente deben estar en un bloque catch. Fuera de la lectura.

No es bueno porque fw se puede cerrar dos veces. (lo cual 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 cerró si excepción al construir un BufferedWriter. (nuevamente, 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(); }