La declaración de prueba con recursos en Java

try-with-resources es una de las varias declaraciones de prueba en Java, destinadas a liberar a los desarrolladores de la obligación de liberar los recursos utilizados en un bloque de prueba. Fue...

Introducción

try-with-resources es una de las varias declaraciones try en Java, destinadas a liberar a los desarrolladores de la obligación de liberar los recursos utilizados en un bloque try.

Se introdujo inicialmente en Java 7 y la idea detrás de esto era que el desarrollador no necesita preocuparse por la administración de recursos para los recursos que usan solo en un bloque try-catch-finally. Esto se logra eliminando la necesidad de bloques “finalmente”, que los desarrolladores solo usaban para cerrar recursos en la práctica.

Además, el código que usa try-with-resources suele ser más limpio y más legible, por lo tanto, hace que el código sea más fácil de administrar, especialmente cuando estamos tratando con muchos bloques try.

Sintaxis

La sintaxis de try-with-resources es casi idéntica a la sintaxis habitual de try-catch-finally. La única diferencia son los paréntesis después de intentar en los que declaramos qué recursos usaremos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
BufferedWriter writer = null;
try {
    writer = new BufferedWriter(new FileWriter(fileName));
    writer.write(str);  // do something with the file we've opened
} catch (IOException e) {
   // handle the exception
} finally {
    try {
        if (writer != null)
            writer.close();
    } catch (IOException e) {
       // handle the exception
    }
}

El mismo código escrito usando try-with-resources se vería así:

1
2
3
4
5
6
try(BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))){
    writer.write(str); // do something with the file we've opened
}
catch(IOException e){
    // handle the exception
}

La forma en que Java entiende este código:

Los recursos abiertos entre paréntesis después de la instrucción try solo serán necesarios aquí y ahora. Llamaré a sus métodos .close() tan pronto como termine con el trabajo en el bloque try. Si se lanza una excepción mientras está en el bloque try, cerraré esos recursos de todos modos.

Antes de que se introdujera este enfoque, el cierre de recursos se realizaba manualmente, como se ve en el código anterior. Esto era esencialmente un código repetitivo, y las bases de código estaban llenas de ellos, lo que reducía la legibilidad y hacía que fueran más difíciles de mantener.

Las partes catch y finally de try-with-resources funcionan como se esperaba, con los bloques catch manejando sus respectivas excepciones y el bloque finally se ejecuta independientemente de si hubo una excepción o no. La única diferencia son las excepciones suprimidas, que se explican al final de este artículo.

Nota: desde Java 9, no es necesario declarar los recursos dentro de la instrucción try-with-resources. Podemos hacer algo como esto en su lugar:

1
2
3
4
5
6
7
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
try (writer) {
    writer.write(str); // do something with the file we've opened
}
catch(IOException e) {
    // handle the exception
}

Múltiples recursos

Otro buen aspecto de probar-con-recursos es la facilidad de agregar/eliminar recursos que estamos usando mientras tenemos la seguridad de que se cerrarán una vez que hayamos terminado.

Si quisiéramos trabajar con varios archivos, abriríamos los archivos en la sentencia try() y los separaríamos con un punto y coma:

1
2
3
4
5
6
7
8
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
    Scanner scanner = new Scanner(System.in)) {
if (scanner.hasNextLine())
    writer.write(scanner.nextLine());
}
catch(IOException e) {
    // handle the exception
}

Luego, Java se encarga de llamar a .close() en todos los recursos que hemos abierto en try().

Nota: Se cierran en orden de declaración inverso, lo que significa que, en nuestro ejemplo, ’escáner’ se cerrará antes que ’escritor'.

Clases compatibles

Todos los recursos declarados en try() deben implementar la interfaz AutoCloseable. Estos suelen ser varios tipos de escritores, lectores, conectores, flujos de salida o entrada, etc. Cualquier cosa que necesite escribir resource.close() después de que haya terminado de trabajar con él.

Esto, por supuesto, incluye objetos definidos por el usuario que implementan la interfaz AutoClosable. Sin embargo, rara vez se encontrará con una situación en la que desee escribir sus propios recursos.

En caso de que eso suceda, debe implementar la interfaz AutoCloseable o Closeable (solo allí para preservar la compatibilidad con versiones anteriores, prefiera AutoCloseable) y anular el método .close():

1
2
3
4
5
6
public class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        // close your resource in the appropriate way
    }
}

Manejo de excepciones {#manejo de excepciones}

Si se lanza una excepción desde dentro de un bloque Java try-with-resources, cualquier recurso abierto dentro de los paréntesis del bloque try aún se cerrará automáticamente.

Como se mencionó anteriormente, try-with-resources funciona igual que try-catch-finally, excepto con una pequeña adición. La adición se llama excepciones suprimidas. No es necesario entender las excepciones suprimidas para usar probar con recursos, pero leer sobre ellas puede ser útil para depurar cuando nada más parece funcionar.

Imagina una situación:

  • Por alguna razón, se produce una excepción en el bloque probar-con-recursos
  • Java detiene la ejecución en el bloque try-with-resources y llama a .close() en todos los recursos declarados en try()
  • Uno de los métodos .close() lanza una excepción
  • ¿Qué excepción sería el bloque catch "catch"?

Esta situación nos introduce a las mencionadas excepciones suprimidas. Una excepción suprimida es una excepción que, de alguna manera, se ignora cuando se lanza dentro del bloque final implícito de un bloque try-with-resources, en el caso de que también se lance una excepción desde el bloque try .

Esas excepciones son excepciones que ocurren en los métodos .close() y se accede a ellas de manera diferente a las excepciones "normales".

Es importante entender que el orden de ejecución es:

  1. bloque probar-con-recursos
  2. implícito finalmente
  3. bloque catch (si se lanzó una excepción en [1] y/o [2])
  4. (explícito) finalmente

Por ejemplo, aquí hay un recurso que no hace nada más que lanzar excepciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static class MyResource implements AutoCloseable {
    // method throws RuntimeException
    public void doSomething() {
        throw new RuntimeException("From the doSomething method");
    }

    // we'll override close so that it throws an exception in the implicit finally block of try-with-resources (when it attempts to close our resource)
    @Override
    public void close() throws Exception {
        throw new ArithmeticException("I can throw whatever I want, you can't stop me.");
    }
}

public static void main(String[] arguments) throws Exception {
    // declare our resource in try
    try (MyResource resource = new MyResource()) {
        resource.doSomething();
    }
    catch (Exception e) {
        System.out.println("Regular exception: " + e.toString());

        // getting the array of suppressed exceptions, and its length
        Throwable[] suppressedExceptions = e.getSuppressed();
        int n = suppressedExceptions.length;

        if (n > 0) {
            System.out.println("We've found " + n + " suppressed exceptions:");
            for (Throwable exception : suppressedExceptions) {
                System.out.println(exception.toString());
            }
        }
    }
}

Este código es ejecutable. Puede usarlo para experimentar con el uso de múltiples objetos MyResource o ver qué sucede cuando try-with-resources no lanza una excepción, pero .close() sí.

Sugerencia: De repente, las excepciones lanzadas al cerrar los recursos comienzan a ser importantes.

Es importante tener en cuenta que en caso de que un recurso arroje una excepción cuando intente cerrarlo, cualquier otro recurso abierto dentro del mismo bloque probar-con-recursos aún se cerrará.

Otro hecho a tener en cuenta es que en una situación en la que el bloque try no arroja una excepción, y donde se lanzan múltiples excepciones al intentar .cerrar() los recursos utilizados, la primera excepción se propagará hacia arriba en la pila de llamadas, mientras que los demás se suprimirán.

Como puede ver en el código, puede obtener la lista de todas las excepciones suprimidas accediendo a la matriz Throwable devuelta por Throwable.getSuppressed().

Recuerde, solo se puede lanzar una sola excepción dentro del bloque de prueba. Tan pronto como se lanza una excepción, se sale del código de bloque de prueba y Java intenta cerrar los recursos.

Conclusión

Siempre que sea posible, se debe usar try-with-resources en lugar de try-catch-finally. Es fácil olvidarse de cerrar uno de sus recursos después de programar durante horas u olvidarse de cerrar un recurso que acaba de agregar a su programa después de un estallido aleatorio de inspiración.

El código es más legible, más fácil de cambiar y mantener y, por lo general, más corto.

Licensed under CC BY-NC-SA 4.0