try-with-resources e AutoCloseable em Java

1. Visão geral

Arquivo, socket, conexão de banco de dados ou sessão com gateway de pagamento são recursos que precisam ser fechados após o uso. Se você esquecer de fechá-los, a aplicação terá vazamento de file descriptor, esgotará o connection pool ou manterá locks por mais tempo do que necessário.

Este artigo apresenta como usar try-with-resources para fechar recursos automaticamente. Você também aprenderá como escrever sua própria classe que implementa AutoCloseable, como Java lida com exceções levantadas ao fechar (exceção suprimida), e os erros mais comuns ao usar essa sintaxe.

2. O problema com try-finally

Antes do Java 7, a forma padrão de fechar recursos era usar um bloco 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, mas tem dois problemas:

  • Verboso. Quando você tem dois ou três recursos, precisa aninhar vários blocos try-finally. Cada bloco ainda precisa fazer verificações de null.
  • Mascara a exceção original. Se readLine() lançar uma IOException, e depois close() também lançar uma exceção, a exceção do finally vai substituir a exceção original. Você perde a informação sobre o erro real.

try-with-resources resolve ambos os problemas.

3. Sintaxe try-with-resources

Você declara o recurso entre parênteses logo após a palavra-chave 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();
        }
    }
}

Quando o bloco try termina, Java chama automaticamente reader.close(). Isso ocorre independentemente de o bloco ser executado normalmente, encontrar um return, ou lançar uma exceção. Você não precisa escrever finally.

O único requisito é que o tipo da variável implemente java.lang.AutoCloseable. A maioria das classes I/O no JDK atende a essa condição, por exemplo InputStream, Reader, Connection, Statement, ResultSet e Scanner.

3.1. Múltiplos recursos e ordem de fechamento

Você pode declarar múltiplos recursos separados por ponto e vírgula. Java fecha-os em ordem reversa à ordem de declaração:

public class ClosingOrderDemo {

    record NamedResource(String name) implements AutoCloseable {
        NamedResource {
            System.out.println("Abrindo " + name);
        }

        @Override
        public void close() {
            System.out.println("Fechando " + 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("Lendo dados do cliente");
        }
    }
}

Resultado:

Abrindo connection
Abrindo statement
Abrindo resultSet
Lendo dados do cliente
Fechando resultSet
Fechando statement
Fechando connection

Esta ordem faz sentido. Um recurso declarado depois geralmente depende do recurso declarado antes, como statement depende de connection, então deve ser fechado primeiro.

Um ponto a lembrar: se o segundo recurso lançar uma exceção logo no constructor, Java ainda fechará o primeiro recurso que foi aberto com sucesso. Os recursos que ainda não foram criados não terão close() chamado.

3.2. Usando variáveis existentes (Java 9+)

A partir do Java 9, você pode colocar na try uma variável já declarada externamente, contanto que seja final ou effectively final:

import java.util.Scanner;

public class CustomerInput {

    public static void main(String[] args) {
        Scanner scanner = new Scanner("João Silva\n0901234567");
        try (scanner) {
            System.out.println("Nome: " + scanner.nextLine());
            System.out.println("Telefone: " + scanner.nextLine());
        }
    }
}

Essa abordagem é conveniente quando o recurso é passado como parâmetro ou criado por uma factory complexa.

3.3. catch e finally executam após o fechamento

try-with-resources ainda permite adicionar catch e finally. No entanto, ambos executam após o recurso ter sido fechado. Portanto, dentro de catch você não pode reutilizar esse recurso, e a variável do recurso também não está mais no escopo.

4. Escrevendo sua própria classe AutoCloseable

AutoCloseable possui apenas um método:

void close() throws Exception;

Suponha que você tenha uma sessão com um gateway de pagamento que precisa ser liberada após cada pedido:

public class PaymentGatewaySession implements AutoCloseable {

    private final String orderId;
    private boolean closed;

    public PaymentGatewaySession(String orderId) {
        this.orderId = orderId;
        System.out.println("Abrindo sessão de pagamento para " + orderId);
    }

    public void charge(long amount) {
        if (closed) {
            throw new IllegalStateException("Sessão já foi fechada");
        }
        if (amount <= 0) {
            throw new IllegalArgumentException("Valor inválido: " + amount);
        }
        System.out.println("Debitando " + amount + " do pedido " + orderId);
    }

    public boolean isClosed() {
        return closed;
    }

    @Override
    public void close() {
        if (closed) {
            return;
        }
        closed = true;
        System.out.println("Fechando sessão de pagamento para " + orderId);
    }
}

Há três pontos importantes nesta classe:

  • close() não declara throws Exception. Um método sobrescrito tem permissão para restringir a lista de exceções verificadas. Assim, os usuários de sua classe não são forçados a fazer catch (Exception e).
  • close() é idempotente: chamar múltiplas vezes não causa erro e não libera o recurso duas vezes.
  • charge() verifica o estado de closed para evitar o uso do recurso após fechá-lo.

Forma de usar:

public class CheckoutService {

    public static void main(String[] args) {
        try (var session = new PaymentGatewaySession("ORD-1001")) {
            session.charge(250_000);
        }
    }
}
Abrindo sessão de pagamento para ORD-1001
Debitando 250000 do pedido ORD-1001
Fechando sessão de pagamento para ORD-1001

4.1. AutoCloseable ou Closeable?

java.io.Closeable existe desde o Java 5 e estende AutoCloseable. Essas duas interfaces diferem nos seguintes pontos:

AutoCloseable Closeable
Exceção de close() Exception IOException
Idempotente Recomendado Obrigatório por contrato
Propósito Qualquer tipo de recurso Recursos I/O

Se seu recurso está relacionado a I/O, use Closeable. Para outros casos, como locks, sessões ou transações, use AutoCloseable e restrinja throws o máximo possível.

5. Exceção suprimida

Voltando ao problema da seção 2: o bloco try lança uma exceção e depois close() também lança uma exceção. Com try-with-resources, a exceção do bloco try é mantida como a exceção principal. A exceção de close() é anexada à exceção principal como suprimida:

public class SuppressedDemo {

    static class FlakyConnection implements AutoCloseable {
        @Override
        public void close() {
            throw new IllegalStateException("Erro ao fechar conexão");
        }
    }

    public static void main(String[] args) {
        try (var connection = new FlakyConnection()) {
            throw new RuntimeException("Erro ao salvar pedido");
        } catch (RuntimeException e) {
            System.out.println("Principal: " + e.getMessage());
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("Suprimida: " + suppressed.getMessage());
            }
        }
    }
}

Resultado:

Principal: Erro ao salvar pedido
Suprimida: Erro ao fechar conexão

Você preserva ambas as informações. Quando imprime o stack trace com e.printStackTrace() ou via logger, a seção Suppressed: também aparece. Isso torna a depuração em produção muito mais fácil.

Se o bloco try executa normalmente e apenas close() lança uma exceção, a exceção de close() será a exceção principal.

6. Testando o comportamento de fechamento de recursos

Para escrever testes, você precisa adicionar JUnit 5 e 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>

O teste abaixo verifica dois comportamentos principais: o recurso é sempre fechado mesmo com erros, e a exceção de close() é anexada à exceção 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("falha ao fechar");
        };

        assertThatThrownBy(() -> {
            try (failingResource) {
                throw new RuntimeException("falha na lógica");
            }
        }).hasMessage("falha na lógica")
          .satisfies(e -> assertThat(e.getSuppressed())
                  .singleElement()
                  .extracting(Throwable::getMessage)
                  .isEqualTo("falha ao fechar"));
    }

    @Test
    void closeShouldBeIdempotent() {
        var session = new PaymentGatewaySession("ORD-3003");
        session.close();
        session.close();

        assertThat(session.isClosed()).isTrue();
        assertThatThrownBy(() -> session.charge(100))
                .isInstanceOf(IllegalStateException.class);
    }
}

O segundo teste usa lambda para criar rapidamente um AutoCloseable. Isso funciona porque AutoCloseable tem apenas um abstract method, então pode ser usado como uma functional interface.

7. Erros comuns

7.1. Aninhar recursos no constructor

try (var in = new ObjectInputStream(new FileInputStream("orders.bin"))) {
    // ...
}

O constructor de ObjectInputStream lê o header do stream e pode lançar IOException. Nesse caso, a variável in não foi atribuída. O FileInputStream interno já foi aberto, mas nunca será fechado. A forma segura é declarar cada recurso separadamente:

try (var file = new FileInputStream("orders.bin");
     var in = new ObjectInputStream(file)) {
    // ...
}

7.2. Retornando o recurso fora do bloco try

public static Stream<String> loadOrderLines(Path path) throws IOException {
    try (Stream<String> lines = Files.lines(path)) {
        return lines; // Errado: stream já foi fechado quando chegar ao chamador
    }
}

O chamador receberá uma IllegalStateException ao tentar usar o stream. Você tem duas formas de corrigir. A primeira é processar todos os dados dentro de try, por exemplo return lines.toList();. A segunda é retornar o stream para fora e deixar que o chamador o envolva em try-with-resources.

Nota: Apenas Streams ligados a recursos I/O precisam ser fechados, como Files.lines, Files.list ou Files.walk. Streams criados a partir de List.stream() não precisam.

7.3. Lançar InterruptedException em close()

O Javadoc de AutoCloseable recomenda não lançar InterruptedException de close(). A razão é que essa exceção pode ser suprimida, perdendo o estado de interrupt da thread. Se você precisar esperar em close(), capture a exceção e chame Thread.currentThread().interrupt().

7.4. Recurso null

Declarar try (var conn = findConnectionOrNull()) é válido. Se o valor for null, Java simplesmente pula a chamada de close(). Porém, isso geralmente é um sinal de design inadequado. Você deve usar Optional ou lançar uma exceção da factory.

7.5. Quando não usar

Nem todo objeto que implementa AutoCloseable deve ser fechado imediatamente em um método. Por exemplo, no Spring Boot, DataSource ou HttpClient compartilhados são beans que vivem junto com o application context. O Spring os fechará quando o contexto for encerrado. Se você envolver um bean compartilhado em try-with-resources, o fechará quando outros componentes ainda o estão usando.

Use try-with-resources apenas para recursos que seu código cria e possui nesse escopo.

7.6. Dica: ExecutorService em Java 21

A partir do Java 19, ExecutorService implementa AutoCloseable. O método close() aguardará a conclusão de todas as tarefas. Isso é muito conveniente ao 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 nota fiscal #" + invoiceId));
            }
        }
        System.out.println("Todas as notas fiscais foram enviadas");
    }
}

A linha “Todas as notas fiscais foram enviadas” é sempre impressa por último porque close() bloqueia até que as três tarefas terminem. A ordem entre as três notas fiscais é indeterminada.

8. Conclusão

try-with-resources fornece código mais conciso e seguro comparado a try-finally. Os recursos são sempre fechados na ordem inversa de abertura. A exceção original não é mascarada graças ao mecanismo de exceção suprimida.

Ao escrever sua própria classe AutoCloseable, você deve:

  • restringir throws de close();
  • tornar close() idempotente;
  • impedir o uso do objeto após fechá-lo.

Além disso, evite aninhar recursos no constructor, não retorne recursos de fora do bloco try, e feche apenas os recursos que seu código realmente possui.

javatry-with-resourcesautocloseableexceptionbest-practices