try-with-resources y AutoCloseable en Java
1. Resumen
Files, sockets, conexiones a base de datos o sesiones de trabajo con una puerta de pago son recursos que necesitan ser cerrados después de usarlos. Si olvidas cerrarlos, tu aplicación sufrirá fugas de file descriptors, agotará el connection pool o mantendrá locks más tiempo del necesario.
Este artículo presenta cómo usar try-with-resources para cerrar recursos automáticamente. También aprenderás cómo escribir tu propia clase que implemente AutoCloseable, cómo Java maneja las excepciones que surgen al cerrar (suppressed exceptions), y los errores comunes al usar esta sintaxis.
2. El problema con try-finally
Antes de Java 7, la forma estándar de cerrar recursos era usar un bloque finally:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class LegacyOrderReader {
public static String readFirstLine(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine();
} finally {
reader.close();
}
}
}
Este código funciona, pero tiene dos problemas:
- Verboso. Cuando tienes dos o tres recursos, necesitas anidar múltiples bloques
try-finally. Cada bloque requiere verificaciones denull. - Oculta la excepción original. Si
readLine()lanza unaIOException, y luegoclose()también lanza una excepción, la excepción delfinallyreemplazará la excepción original. Pierdes la información sobre el error real.
try-with-resources resuelve ambos problemas.
3. Sintaxis de try-with-resources
Declaras el recurso entre paréntesis justo después de la palabra clave try:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class OrderReader {
public static String readFirstLine(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
}
}
Cuando el bloque try termina, Java automáticamente llama a reader.close(). Esto ocurre tanto si el bloque se ejecuta normalmente, si hay un return, como si hay una excepción. No necesitas escribir finally.
La única condición es que el tipo de la variable implemente java.lang.AutoCloseable. La mayoría de clases de I/O en el JDK cumplen esta condición, por ejemplo InputStream, Reader, Connection, Statement, ResultSet y Scanner.
3.1. Múltiples recursos y orden de cierre
Puedes declarar múltiples recursos, separados por punto y coma. Java los cierra en orden inverso al de su declaración:
public class ClosingOrderDemo {
record NamedResource(String name) implements AutoCloseable {
NamedResource {
System.out.println("Abriendo " + name);
}
@Override
public void close() {
System.out.println("Cerrando " + name);
}
}
public static void main(String[] args) {
try (var connection = new NamedResource("connection");
var statement = new NamedResource("statement");
var resultSet = new NamedResource("resultSet")) {
System.out.println("Leyendo datos de cliente");
}
}
}
Resultado:
Abriendo connection
Abriendo statement
Abriendo resultSet
Leyendo datos de cliente
Cerrando resultSet
Cerrando statement
Cerrando connection
Este orden tiene sentido. Los recursos declarados después típicamente dependen de los declarados antes, como statement depende de connection, así que deben cerrarse primero.
Hay un punto importante a recordar. Si el constructor del segundo recurso lanza una excepción, Java aún cerrará el primer recurso que fue abierto exitosamente. Los recursos que aún no fueron creados no tendrán su close() llamado.
3.2. Usar una variable existente (Java 9+)
A partir de Java 9, puedes incluir en try una variable ya declarada fuera, mientras sea final o efectivamente final:
import java.util.Scanner;
public class CustomerInput {
public static void main(String[] args) {
Scanner scanner = new Scanner("Nombre\nTeléfono");
try (scanner) {
System.out.println("Nombre: " + scanner.nextLine());
System.out.println("Teléfono: " + scanner.nextLine());
}
}
}
Esta forma es conveniente cuando el recurso se pasa como parámetro o es creado por una factory compleja.
3.3. Los bloques catch y finally se ejecutan después de cerrar
try-with-resources aún permite añadir catch y finally. Sin embargo, ambos se ejecutan después de que el recurso ha sido cerrado. Por eso, dentro del catch no puedes volver a usar el recurso, y la variable tampoco está en el scope.
4. Escribir tu propia clase AutoCloseable
AutoCloseable tiene un solo método:
void close() throws Exception;
Supongamos que tienes una sesión de trabajo con una puerta de pago, y esta sesión debe ser liberada después de cada pedido:
public class PaymentGatewaySession implements AutoCloseable {
private final String orderId;
private boolean closed;
public PaymentGatewaySession(String orderId) {
this.orderId = orderId;
System.out.println("Abriendo sesión de pago para " + orderId);
}
public void charge(long amount) {
if (closed) {
throw new IllegalStateException("Sesión cerrada");
}
if (amount <= 0) {
throw new IllegalArgumentException("Monto inválido: " + amount);
}
System.out.println("Debitando " + amount + " VND para pedido " + orderId);
}
public boolean isClosed() {
return closed;
}
@Override
public void close() {
if (closed) {
return;
}
closed = true;
System.out.println("Cerrando sesión de pago para " + orderId);
}
}
Hay tres puntos dignos de mención en esta clase:
close()no declarathrows Exception. Un método que hace override puede restringir la lista de excepciones verificadas. Gracias a esto, los usuarios de tu clase no están obligados a hacercatch (Exception e).close()es idempotente: llamarla múltiples veces no causa errores y no libera dos veces.charge()verifica el estadoclosedpara prevenir el uso del recurso después de cerrarlo.
Cómo usarlo:
public class CheckoutService {
public static void main(String[] args) {
try (var session = new PaymentGatewaySession("ORD-1001")) {
session.charge(250_000);
}
}
}
Abriendo sesión de pago para ORD-1001
Debitando 250000 VND para pedido ORD-1001
Cerrando sesión de pago para ORD-1001
4.1. ¿AutoCloseable o Closeable?
java.io.Closeable existe desde Java 5 y extiende AutoCloseable. Estos dos interfaces difieren en los siguientes puntos:
AutoCloseable |
Closeable |
|
|---|---|---|
Excepción de close() |
Exception |
IOException |
| Idempotente | Recomendado | Obligatorio por contrato |
| Propósito | Cualquier tipo de recurso | Recursos de I/O |
Si tu recurso está relacionado con I/O, usa Closeable. Para otros casos, como locks, sesiones de trabajo o transacciones, usa AutoCloseable y limita throws tanto como sea posible.
5. Excepciones suprimidas
Volvamos al problema de la sección 2: el bloque try lanza una excepción, y luego close() también lanza una. Con try-with-resources, la excepción del bloque try se conserva como excepción principal. La excepción de close() se adjunta a la excepción principal como suprimida:
public class SuppressedDemo {
static class FlakyConnection implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("Error al cerrar la conexión");
}
}
public static void main(String[] args) {
try (var connection = new FlakyConnection()) {
throw new RuntimeException("Error al guardar el pedido");
} catch (RuntimeException e) {
System.out.println("Principal: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suprimida: " + suppressed.getMessage());
}
}
}
}
Resultado:
Principal: Error al guardar el pedido
Suprimida: Error al cerrar la conexión
Obtienes ambas piezas de información. Cuando imprimes el stack trace usando e.printStackTrace() o a través de un logger, la sección Suppressed: también aparece. Gracias a esto, el debug en producción es mucho más fácil.
Si el bloque try se ejecuta normalmente y solo close() lanza una excepción, entonces la excepción de close() será la excepción principal.
6. Testeando el comportamiento de cierre
Para escribir tests, necesitas agregar JUnit 5 y AssertJ:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
El siguiente test verifica dos comportamientos principales: el recurso siempre se cierra aunque haya un error, y la excepción de close() se adjunta a la excepción principal como suprimida.
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class PaymentGatewaySessionTest {
@Test
void shouldCloseSessionEvenWhenChargeFails() {
var session = new PaymentGatewaySession("ORD-2002");
assertThatThrownBy(() -> {
try (session) {
session.charge(-5);
}
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("-5");
assertThat(session.isClosed()).isTrue();
}
@Test
void shouldAttachCloseFailureAsSuppressed() {
AutoCloseable failingResource = () -> {
throw new IllegalStateException("cierre falló");
};
assertThatThrownBy(() -> {
try (failingResource) {
throw new RuntimeException("negocio falló");
}
}).hasMessage("negocio falló")
.satisfies(e -> assertThat(e.getSuppressed())
.singleElement()
.extracting(Throwable::getMessage)
.isEqualTo("cierre falló"));
}
@Test
void closeShouldBeIdempotent() {
var session = new PaymentGatewaySession("ORD-3003");
session.close();
session.close();
assertThat(session.isClosed()).isTrue();
assertThatThrownBy(() -> session.charge(100))
.isInstanceOf(IllegalStateException.class);
}
}
El segundo test usa una lambda para crear rápidamente un AutoCloseable. Esto funciona porque AutoCloseable solo tiene un método abstracto, por lo que puede usarse como un functional interface.
7. Errores comunes
7.1. Anidar recursos en el constructor
try (var in = new ObjectInputStream(new FileInputStream("orders.bin"))) {
// ...
}
El constructor de ObjectInputStream lee la cabecera del stream y puede lanzar una IOException. En ese caso, la variable in aún no ha sido asignada. El FileInputStream interior ya se abrió pero nunca será cerrado. La forma segura es declarar cada recurso por separado:
try (var file = new FileInputStream("orders.bin");
var in = new ObjectInputStream(file)) {
// ...
}
7.2. Retornar un recurso fuera del bloque try
public static Stream<String> loadOrderLines(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path)) {
return lines; // Incorrecto: el stream está cerrado cuando llega al llamador
}
}
El llamador recibirá una IllegalStateException apenas intente usar el stream. Tienes dos formas de arreglarlo. La primera es procesar todos los datos dentro del try, por ejemplo return lines.toList();. La segunda es retornar el stream fuera y dejar que el llamador lo envuelva en try-with-resources.
Nota: Solo los
Streamvinculados a recursos de I/O necesitan ser cerrados, por ejemploFiles.lines,Files.listoFiles.walk. Los streams creados desdeList.stream()no lo requieren.
7.3. Lanzar InterruptedException en close()
El Javadoc de AutoCloseable aconseja no lanzar InterruptedException desde close(). La razón es que esta excepción puede ser suprimida, lo que causa que el estado de interrupción del thread se pierda. Si es necesario esperar en close(), atrapa la excepción y luego llama a Thread.currentThread().interrupt().
7.4. Recurso null
Declarar try (var conn = findConnectionOrNull()) es válido. Si el valor es null, Java simplemente omite llamar a close(). Sin embargo, esto generalmente es un signo de un diseño no ideal. Deberías usar Optional o lanzar una excepción desde la factory.
7.5. Cuándo no usarlo
No todo objeto que implementa AutoCloseable debe cerrarse inmediatamente dentro de un método. Por ejemplo, en Spring Boot, DataSource o HttpClient compartidos son beans que viven con el contexto de la aplicación. Spring los cerrará cuando el contexto se apague. Si envuelves un bean compartido en try-with-resources, lo cerrarás cuando otros componentes aún lo estén usando.
Solo usa try-with-resources para recursos que tu código crea y posee dentro de ese alcance.
7.6. Consejo: ExecutorService en Java 21
A partir de Java 19, ExecutorService implementa AutoCloseable. El método close() espera a que todas las tareas se completen. Esto es muy conveniente al usar virtual threads:
import java.util.concurrent.Executors;
public class InvoiceBatch {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 1; i <= 3; i++) {
int invoiceId = i;
executor.submit(() -> System.out.println("Enviando factura #" + invoiceId));
}
}
System.out.println("Se han enviado todas las facturas");
}
}
La línea “Se han enviado todas las facturas” siempre se imprime al final, porque close() bloquea hasta que las tres tareas se completen. El orden entre las tres facturas no es garantizado.
8. Conclusión
try-with-resources hace que el código sea más corto y seguro comparado con try-finally. Los recursos siempre se cierran, en orden inverso al de su apertura. La excepción original no se oculta, gracias al mecanismo de excepciones suprimidas.
Cuando escribas tu propia clase AutoCloseable, deberías:
- limitar
throwsdeclose(); - hacer que
close()sea idempotente; - prevenir el uso del objeto después de cerrarlo.
Además, evita anidar recursos en el constructor, no retornes un recurso fuera del bloque try, y solo cierra los recursos que tu código realmente posee.