Java Essencial: Tratamento de exceções e criação de erros significativos

Capítulo 15

Tempo estimado de leitura: 9 minutos

+ Exercício

O que é uma exceção e por que ela existe

Uma exceção é um evento que interrompe o fluxo normal do programa quando algo inesperado acontece (por exemplo: entrada inválida, arquivo inexistente, divisão por zero). Em Java, exceções são objetos (subclasses de Throwable) que carregam informações sobre o problema e permitem que você decida entre: recuperar (continuar com um plano alternativo), falhar com mensagem clara (erro significativo) ou propagar (deixar camadas superiores decidirem).

Uma boa estratégia de exceções busca: mensagens úteis, stack trace preservado, encapsulamento (não vazar detalhes internos) e fluxos de recuperação quando fizer sentido.

try/catch/finally: controle do fluxo em caso de falha

Estrutura básica

try {    // código que pode falhar} catch (AlgumaException e) {    // tratamento/recuperação} finally {    // sempre executa (com ou sem exceção)}
  • try: envolve o trecho que pode lançar exceções.
  • catch: captura exceções específicas para tratar (ou transformar) o erro.
  • finally: executa independentemente de sucesso/erro; útil para liberar recursos quando não se usa try-with-resources.

Ordem e especificidade dos catches

Coloque exceções mais específicas antes das genéricas. Caso contrário, o compilador reclama porque o catch genérico “engole” os específicos.

try {    // ...} catch (NumberFormatException e) {    // específico} catch (RuntimeException e) {    // mais genérico}

Multi-catch

Quando o tratamento é igual para mais de um tipo:

try {    // ...} catch (java.io.IOException | java.nio.file.InvalidPathException e) {    System.err.println("Falha ao acessar caminho/arquivo: " + e.getMessage());}

Exceções checadas vs não checadas (checked vs unchecked)

Checked exceptions

São exceções que o compilador obriga você a tratar (try/catch) ou declarar (throws). Em geral, representam condições que podem ocorrer em tempo de execução e que o chamador pode razoavelmente tentar recuperar (ex.: I/O).

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Exemplo comum: IOException.

Unchecked exceptions

São subclasses de RuntimeException. O compilador não exige tratamento. Normalmente indicam erro de programação, pré-condições violadas ou estados inválidos (ex.: NullPointerException, IllegalArgumentException, ArithmeticException).

Regra prática

  • Use checked quando o chamador pode agir (tentar outro arquivo, repetir operação, pedir novo input).
  • Use unchecked para validação de argumentos e invariantes (o código não deveria ter chamado daquele jeito).

Stack trace: como ler e como preservar

O stack trace mostra a cadeia de chamadas que levou ao erro, do ponto onde a exceção foi lançada até o ponto onde foi tratada (ou até o programa terminar). Ele é essencial para depuração.

Exemplo de saída (simplificada)

Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"    at java.lang.Integer.parseInt(Integer.java:...)    at app.Calculadora.parseEntrada(Calculadora.java:12)    at app.Main.main(Main.java:7)

Não perca a causa original

Um erro comum é capturar e lançar outra exceção sem encadear a causa, perdendo o stack trace original. Prefira:

catch (java.io.IOException e) {    throw new RuntimeException("Falha ao ler o arquivo de configuração", e);}

O segundo argumento (e) preserva a causa e facilita muito o diagnóstico.

Mensagens úteis: o que escrever (e o que evitar)

Checklist de uma boa mensagem

  • O que falhou (ação): “Não foi possível carregar o arquivo”.
  • Onde/qual recurso: caminho, id, parâmetro (sem expor segredos).
  • Por quê (quando disponível): mensagem da exceção original ou contexto.
  • O que o usuário pode fazer: “Verifique se o arquivo existe e se há permissão”.

Evite

  • Mensagens genéricas: “Erro ocorreu”.
  • Vazar detalhes sensíveis (tokens, senhas, conteúdo de arquivo).
  • Engolir exceções sem registrar/propagar: catch (Exception e) {}.

Encapsulamento de exceções (camadas e responsabilidades)

Em aplicações com camadas (ex.: leitura de arquivo, regras de negócio, interface), é comum encapsular exceções técnicas em exceções mais significativas para a camada acima.

Exemplo: transformar IOException em erro de domínio

public class ConfigService {    public String carregarConfig(String caminho) {        try {            return java.nio.file.Files.readString(java.nio.file.Path.of(caminho));        } catch (java.io.IOException e) {            // Encapsula detalhe técnico em uma exceção mais clara para o chamador            throw new ConfiguracaoException("Não foi possível ler a configuração em: " + caminho, e);        }    }}

Assim, quem chama ConfigService lida com ConfiguracaoException (mais semântica), mas o stack trace original continua disponível via getCause().

try-with-resources: fechamento automático em I/O

Recursos como streams, readers e writers precisam ser fechados. O try-with-resources garante fechamento automático mesmo se ocorrer exceção, reduzindo vazamentos e simplificando código. Ele funciona com tipos que implementam AutoCloseable.

Passo a passo: ler um arquivo com tratamento adequado

Objetivo: ler a primeira linha de um arquivo e lidar com “arquivo inexistente” e outros problemas de I/O.

  1. Defina o caminho do arquivo.
  2. Abra o recurso dentro do try (...).
  3. Leia o conteúdo.
  4. Trate exceções específicas com mensagens úteis.
import java.io.BufferedReader;import java.io.IOException;import java.nio.file.Files;import java.nio.file.NoSuchFileException;import java.nio.file.Path;public class LeitorPrimeiraLinha {    public static String lerPrimeiraLinha(String caminho) {        Path path = Path.of(caminho);        try (BufferedReader br = Files.newBufferedReader(path)) {            return br.readLine();        } catch (NoSuchFileException e) {            throw new IllegalStateException("Arquivo não encontrado: " + caminho + ". Verifique o caminho.", e);        } catch (IOException e) {            throw new IllegalStateException("Falha de I/O ao ler: " + caminho + ". Verifique permissões.", e);        }    }}

Note que o recurso (BufferedReader) é fechado automaticamente. As exceções são encapsuladas com mensagens orientadas ao que fazer.

Detalhe importante: exceções no close

Se ocorrer exceção durante o fechamento, o Java pode anexá-la como suppressed à exceção principal. Isso é útil para diagnóstico e aparece no stack trace.

Criação de exceções customizadas

Exceções customizadas ajudam a expressar erros do seu domínio (ex.: “PagamentoRecusado”, “ConfiguracaoInvalida”). Em geral, crie uma classe com construtores que aceitem mensagem e causa.

Exemplo: exceção de configuração

public class ConfiguracaoException extends RuntimeException {    public ConfiguracaoException(String message) {        super(message);    }    public ConfiguracaoException(String message, Throwable cause) {        super(message, cause);    }}

Quando usar checked vs unchecked em customizadas

  • Unchecked (extends RuntimeException): quando é erro de estado/ambiente que você não quer obrigar o chamador a tratar em todo lugar, mas ainda quer semântica clara.
  • Checked (extends Exception): quando o chamador deve decidir explicitamente como recuperar (ex.: operação que pode falhar e tem alternativas).

Validação de argumentos com IllegalArgumentException

Quando um método recebe parâmetros inválidos, a exceção mais comum é IllegalArgumentException. Ela é unchecked e comunica que o chamador violou uma pré-condição.

Passo a passo: validar entrada antes de processar

  1. Liste as pré-condições (ex.: não nulo, não vazio, faixa numérica).
  2. Valide no início do método (fail fast).
  3. Escreva mensagem indicando o parâmetro e a regra.
public class Validador {    public static int parseIdPositivo(String texto) {        if (texto == null) {            throw new IllegalArgumentException("Parâmetro 'texto' não pode ser null");        }        String t = texto.trim();        if (t.isEmpty()) {            throw new IllegalArgumentException("Parâmetro 'texto' não pode ser vazio");        }        try {            int id = Integer.parseInt(t);            if (id <= 0) {                throw new IllegalArgumentException("Id deve ser > 0. Valor recebido: " + id);            }            return id;        } catch (NumberFormatException e) {            throw new IllegalArgumentException("Id inválido: '" + texto + "'. Informe um número inteiro.", e);        }    }}

Repare no encadeamento da causa ao transformar NumberFormatException em IllegalArgumentException com mensagem mais orientada ao usuário.

Cenários práticos e fluxos de recuperação

1) Entrada inválida (número digitado errado)

Cenário: o usuário informa “abc” quando o sistema espera um inteiro.

Estratégia: validar e pedir novamente (recuperação) ou falhar com mensagem clara (em APIs).

Exemplo: loop de recuperação no console

import java.util.Scanner;public class EntradaComRecuperacao {    public static int lerInteiro(Scanner sc, String rotulo) {        while (true) {            System.out.print(rotulo);            String s = sc.nextLine();            try {                return Integer.parseInt(s.trim());            } catch (NumberFormatException e) {                System.out.println("Valor inválido: '" + s + "'. Digite um número inteiro.");            }        }    }}

Aqui a exceção é usada para controlar a recuperação: ao falhar, o programa informa e tenta novamente.

2) Arquivo inexistente

Cenário: caminho errado ou arquivo removido.

Estratégia: tratar NoSuchFileException separadamente para mensagem específica; oferecer alternativa (ex.: usar padrão) quando aplicável.

import java.io.IOException;import java.nio.file.Files;import java.nio.file.NoSuchFileException;import java.nio.file.Path;public class Carregador {    public static String carregarOuPadrao(String caminho, String padrao) {        try {            return Files.readString(Path.of(caminho));        } catch (NoSuchFileException e) {            // Recuperação: usa valor padrão            return padrao;        } catch (IOException e) {            // Não recuperável aqui: propaga com contexto            throw new IllegalStateException("Falha ao ler arquivo: " + caminho, e);        }    }}

3) Divisão por zero

Cenário: denominador igual a zero.

Estratégia: evitar depender de ArithmeticException para regra de negócio; valide antes e retorne erro significativo.

public class Matematica {    public static double dividir(double numerador, double denominador) {        if (denominador == 0.0) {            throw new IllegalArgumentException("Denominador não pode ser zero");        }        return numerador / denominador;    }}

Se você estiver trabalhando com int, a divisão por zero lança ArithmeticException. Mesmo assim, preferir validação explícita costuma gerar mensagens melhores e regras mais claras.

4) Escrevendo mensagens e definindo o ponto certo de tratamento

Decida onde tratar com base em quem consegue agir:

  • Camada de interface (console/UI/API): geralmente trata e mostra mensagem amigável.
  • Camada de serviço/regra: pode validar, encapsular e propagar.
  • Camada de infraestrutura (I/O): captura exceções técnicas e adiciona contexto (caminho, operação).

Exemplo: fluxo completo com encapsulamento e mensagem amigável

import java.util.Scanner;public class App {    public static void main(String[] args) {        try (Scanner sc = new Scanner(System.in)) {            System.out.print("Informe o caminho do arquivo: ");            String caminho = sc.nextLine();            String conteudo = new ConfigService().carregarConfig(caminho);            System.out.println("Config carregada. Tamanho: " + conteudo.length());        } catch (ConfiguracaoException e) {            // Mensagem amigável para o usuário            System.out.println(e.getMessage());            // Em um app real, você também registraria o stack trace em log            // e.printStackTrace();        }    }}

Neste fluxo, a interface mostra uma mensagem clara (sem detalhes técnicos excessivos), enquanto a exceção mantém a causa original para diagnóstico.

Tabela rápida: escolhas comuns

SituaçãoExceção típicaAbordagem recomendada
Argumento inválido (null, vazio, fora de faixa)IllegalArgumentExceptionValidar no início do método e falhar com mensagem específica
Entrada textual não é númeroNumberFormatExceptionRecuperar pedindo novamente (UI) ou encapsular em IllegalArgumentException (API)
Arquivo não existeNoSuchFileExceptionMensagem específica; opcionalmente recuperar com padrão
Falha geral de I/OIOExceptionTry-with-resources; encapsular com contexto e preservar causa
Regra de negócio violadaExceção customizadaNome semântico, mensagem clara, causa quando houver

Agora responda o exercício sobre o conteúdo:

Ao capturar uma IOException e lançar uma nova exceção para a camada acima, qual prática ajuda a manter um diagnóstico útil sem vazar detalhes internos desnecessários?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Encadear a causa preserva o stack trace original e facilita depuração, enquanto a mensagem pode trazer contexto útil (ex.: caminho/ação) sem expor detalhes sensíveis.

Próximo capitúlo

Java Essencial: Maven para projetos Java (dependências, build e execução)

Arrow Right Icon
Capa do Ebook gratuito Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)
83%

Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.