Java Criar suas Exceções

A biblioteca do Java tem dezenas de exceções prontas, mas nenhuma se chama "saldo insuficiente" ou "aluno não encontrado". Quando um problema é do seu programa, você pode criar uma exceção com o nome dele. É só uma classe que herda de Exception (ou de RuntimeException), e você já sabe tudo o que precisa para escrevê-la.

Você vai aprender
  • Criar uma exceção checked: SaldoInsuficienteException extends Exception.
  • Escrever o construtor com mensagem usando super(mensagem).
  • Guardar informações extras na exceção (quanto faltou).
  • Criar a versão unchecked, com extends RuntimeException, e comparar as duas.
  • Embrulhar uma exceção em outra, guardando a causa: new X("…", e) e o Caused by:.
  • Quando vale a pena criar uma exceção, e quando uma pronta resolve.

Esta aula usa herança, super e tudo das aulas anteriores de exceções, principalmente Checked × Unchecked.


A ideia antes do código

Num hospital, ninguém grita "deu problema!". Gritam "parada cardíaca no quarto 12": o nome do problema já diz quem deve vir e o que fazer. Com exceções é igual. Compare as duas primeiras linhas de um stack trace:

Exception in thread "main" java.lang.IllegalStateException: saldo de 70.0 não cobre o saque de 80.0
Exception in thread "main" SaldoInsuficienteException: saldo de 70.0 não cobre o saque de 80.0

A segunda fala a língua do negócio da Ana. E, mais importante: quem chama o método pode escrever catch (SaldoInsuficienteException e) e tratar exatamente esse caso, sem capturar por engano outros problemas de estado.


Criando a exceção

Uma exceção própria é uma classe comum, com duas particularidades: ela herda de uma classe da família Throwable e repassa a mensagem para a classe mãe. Por convenção, o nome termina em Exception. Como toda classe pública, ela fica no seu próprio arquivo:

public class SaldoInsuficienteException extends Exception {
    public SaldoInsuficienteException(String mensagem) {
        super(mensagem);
    }
}
public class SaldoInsuficienteException
Uma classe pública, no arquivo SaldoInsuficienteException.java. O nome descreve o problema e termina em Exception.
extends Exception
"Herda de Exception". Isso a coloca na família das exceções (pode ser lançada e capturada) e a torna checked: quem chamar um método que a lança terá que tratar ou declarar.
public SaldoInsuficienteException(String mensagem)
O construtor, que recebe o texto que explica o problema.
super(mensagem);
Chama o construtor da classe mãe (Exception) passando a mensagem. É a Exception quem guarda o texto e o devolve em getMessage() e no stack trace. Sem esta linha, a mensagem se perde.

Pronto: ela já tem getMessage(), printStackTrace(), stack trace, tudo, herdado de Throwable. Agora a classe Conta pode usá-la:

public class Conta {
    private String titular;
    private double saldo;

    public Conta(String titular, double saldo) {
        this.titular = titular;
        this.saldo = saldo;
    }

    public void sacar(double valor) throws SaldoInsuficienteException {
        if (valor > saldo) {
            throw new SaldoInsuficienteException(
                "saldo de " + saldo + " não cobre o saque de " + valor);
        }
        saldo -= valor;
        System.out.println(titular + " sacou " + valor + ". Saldo: " + saldo);
    }
}
public class Main {
    public static void main(String[] args) {
        Conta conta = new Conta("Ana", 100.0);
        try {
            conta.sacar(30.0);
            conta.sacar(80.0);
            conta.sacar(10.0);
        } catch (SaldoInsuficienteException e) {
            System.out.println("Saque negado: " + e.getMessage());
        }
    }
}

São três arquivos na mesma pasta. O javac *.java compila todos de uma vez (o * é o curinga do terminal: "todos os arquivos que terminam em .java"):

$ javac *.java
$ java Main
Ana sacou 30.0. Saldo: 70.0
Saque negado: saldo de 70.0 não cobre o saque de 80.0

O primeiro saque passou; o segundo (80, com saldo 70) lançou a exceção, o catch a capturou pelo nome e o terceiro saque nem foi tentado. No JDK 25, java Main.java também funciona com vários arquivos: ele encontra as outras classes na mesma pasta.

Ela é checked: o compilador cobra

Como herda de Exception, a sua exceção ganha as regras de checked. O sacar precisou do throws SaldoInsuficienteException, e quem chama precisa tratar. Sem o try no main:

public class Main {
    public static void main(String[] args) {
        Conta conta = new Conta("Ana", 100.0);
        conta.sacar(30.0);
        conta.sacar(80.0);
    }
}
$ javac *.java
Main.java:4: error: unreported exception SaldoInsuficienteException; must be caught or declared to be thrown
        conta.sacar(30.0);
                   ^
Main.java:5: error: unreported exception SaldoInsuficienteException; must be caught or declared to be thrown
        conta.sacar(80.0);
                   ^
2 errors

O mesmo erro da IOException, agora com o nome da sua classe. Um erro por chamada: o javac não sabe qual saque vai falhar, então cobra um plano para todos.

Missão: a classe SaldoInsuficienteException (na primeira aba) está vazia, e o programa não compila. Faça-a herdar de Exception e escreva o construtor que recebe a mensagem e a repassa com super(mensagem). As outras abas já estão prontas.

public class SaldoInsuficienteException {
}
public class Conta {
    private String titular;
    private double saldo;

    public Conta(String titular, double saldo) {
        this.titular = titular;
        this.saldo = saldo;
    }

    public void sacar(double valor) throws SaldoInsuficienteException {
        if (valor > saldo) {
            throw new SaldoInsuficienteException(
                "saldo de " + saldo + " não cobre o saque de " + valor);
        }
        saldo -= valor;
        System.out.println(titular + " sacou " + valor + ". Saldo: " + saldo);
    }
}
public class Main {
    public static void main(String[] args) {
        Conta conta = new Conta("Ana", 100.0);
        try {
            conta.sacar(30.0);
            conta.sacar(80.0);
            conta.sacar(10.0);
        } catch (SaldoInsuficienteException e) {
            System.out.println("Saque negado: " + e.getMessage());
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Conta conta = new Conta("Ana", 100.0);
        try {
            conta.sacar(30.0);
            conta.sacar(80.0);
            conta.sacar(10.0);
        } catch (SaldoInsuficienteException e) {
            System.out.println("Saque negado: " + e.getMessage());
        }
    }
}
public class SaldoInsuficienteException extends Exception {
    public SaldoInsuficienteException(String mensagem) {
        super(mensagem);
    }
}
public class Conta {
    private String titular;
    private double saldo;

    public Conta(String titular, double saldo) {
        this.titular = titular;
        this.saldo = saldo;
    }

    public void sacar(double valor) throws SaldoInsuficienteException {
        if (valor > saldo) {
            throw new SaldoInsuficienteException(
                "saldo de " + saldo + " não cobre o saque de " + valor);
        }
        saldo -= valor;
        System.out.println(titular + " sacou " + valor + ". Saldo: " + saldo);
    }
}

Informações extras na exceção

Como é uma classe sua, a exceção pode ter atributos e métodos. Em vez de quem captura ter que "ler" a mensagem para saber quanto faltou, a exceção entrega o número pronto:

public class SaldoInsuficienteException extends Exception {
    private final double faltando;

    public SaldoInsuficienteException(double saldo, double valor) {
        super("saldo de " + saldo + " não cobre o saque de " + valor);
        this.faltando = valor - saldo;
    }

    public double getFaltando() {
        return faltando;
    }
}
private final double faltando;
Um atributo com quanto falta. final porque uma exceção descreve algo que já aconteceu: não muda depois de criada.
(double saldo, double valor)
Agora o construtor recebe os números, não o texto pronto…
super("saldo de " + …)
…e monta a mensagem ele mesmo. O super(…) tem que ser a primeira coisa do construtor.
getFaltando()
Um get para quem capturar.

Na Conta, o throw fica mais curto: throw new SaldoInsuficienteException(saldo, valor);. E o catch do main ganha uma linha:

        } catch (SaldoInsuficienteException e) {
            System.out.println("Saque negado: " + e.getMessage());
            System.out.println("Faltam R$ " + e.getFaltando());
        }
$ javac *.java
$ java Main
Ana sacou 30.0. Saldo: 70.0
Saque negado: saldo de 70.0 não cobre o saque de 80.0
Faltam R$ 10.0

Com o valor em mãos, o programa poderia oferecer um saque de R$ 70 ou sugerir um depósito de R$ 10. Para dinheiro de verdade, a Ana deveria usar BigDecimal em vez de double; veja BigDecimal e Dinheiro.


A versão unchecked

Trocando a mãe de Exception para RuntimeException, a exceção vira unchecked. Nada mais muda na classe:

public class SaldoInsuficienteException extends RuntimeException {
    public SaldoInsuficienteException(String mensagem) {
        super(mensagem);
    }
}

Agora o sacar não precisa de throws e o main compila sem try. O mesmo main que deu dois erros de compilação há pouco:

$ javac *.java
$ java Main
Ana sacou 30.0. Saldo: 70.0
Exception in thread "main" SaldoInsuficienteException: saldo de 70.0 não cobre o saque de 80.0
	at Conta.sacar(Conta.java:12)
	at Main.main(Main.java:5)

Compilou, rodou, e a exceção apareceu na execução, sem tratamento. Repare no nome sem pacote (SaldoInsuficienteException, e não java.lang.…): a classe está no pacote padrão, sem package (veja Pacotes e import). E o stack trace agora passa por dois arquivos: Conta.java linha 12 e Main.java linha 5.

extends Exception (checked)extends RuntimeException (unchecked)
throws no métodoObrigatórioOpcional (vale como documentação)
Quem chamaPrecisa tratar ou declarar, senão não compilaTrata se quiser
VantagemNinguém esquece de pensar no casoCódigo mais limpo; os métodos do meio não mudam
Combina comSituações esperadas que quem chama deve resolverRegras violadas por erro de quem chama; projetos que preferem unchecked

Para o saldo, as duas escolhas são defensáveis. Se o programa deve sempre dizer algo ao cliente quando falta saldo, checked garante isso. Se o código que chama sacar sempre confere o saldo antes (e a exceção só aconteceria por um defeito), unchecked é mais simples.


A causa: uma exceção dentro da outra

A Ana importa alunos de uma lista de linhas no formato nome,idade. Quando a idade não é número, o parseInt lança NumberFormatException. Mas, para quem chamou a importação, "For input string: "trinta"" não diz qual linha estava errada. O certo é lançar uma exceção da importação, com uma mensagem útil, sem perder a original. A original vira a causa (cause):

public class ImportacaoException extends Exception {
    public ImportacaoException(String mensagem, Throwable causa) {
        super(mensagem, causa);
    }
}
Throwable causa
Um segundo parâmetro: a exceção original. O tipo é Throwable para aceitar qualquer uma.
super(mensagem, causa);
A Exception tem um construtor que recebe as duas coisas e guarda a causa. Depois, ela aparece em getCause() e no stack trace.
public class Main {
    static int lerIdade(String linha, int numero) throws ImportacaoException {
        String[] partes = linha.split(",");
        try {
            return Integer.parseInt(partes[1]);
        } catch (NumberFormatException e) {
            throw new ImportacaoException("linha " + numero + ": idade inválida", e);
        }
    }

    public static void main(String[] args) throws ImportacaoException {
        String[] linhas = {"Ana,29", "Bruno,trinta", "Carla,22"};
        for (int i = 0; i < linhas.length; i++) {
            int idade = lerIdade(linhas[i], i + 1);
            System.out.println("Idade lida: " + idade);
        }
    }
}

O split(",") corta a linha na vírgula (veja Métodos de String), e partes[1] é a idade. O catch não "engole" a NumberFormatException: ele lança outra exceção, mais significativa, levando a original junto (o e no fim). Isso se chama encadear exceções. Sem ninguém tratando, o stack trace mostra a corrente inteira:

$ javac *.java
$ java Main
Idade lida: 29
Exception in thread "main" ImportacaoException: linha 2: idade inválida
	at Main.lerIdade(Main.java:7)
	at Main.main(Main.java:14)
Caused by: java.lang.NumberFormatException: For input string: "trinta"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Main.lerIdade(Main.java:5)
	... 1 more
ImportacaoException: linha 2: idade inválida
A exceção de cima, com a mensagem que diz onde no arquivo está o problema. Ela foi lançada na linha 7.
Caused by: java.lang.NumberFormatException: For input string: "trinta"
"Causada por": a original, com o motivo técnico. Ela nasceu dentro do parseInt, chamado na linha 5.
... 1 more
"Mais 1": a causa tem mais uma moldura que é igual à última da exceção de cima (Main.main(Main.java:14)); o Java a omite para não repetir.

Tratando no main, getCause() devolve a original:

    public static void main(String[] args) {
        String[] linhas = {"Ana,29", "Bruno,trinta", "Carla,22"};
        for (int i = 0; i < linhas.length; i++) {
            try {
                int idade = lerIdade(linhas[i], i + 1);
                System.out.println("Idade lida: " + idade);
            } catch (ImportacaoException e) {
                System.out.println("Problema: " + e.getMessage());
                System.out.println("Causa: " + e.getCause());
            }
        }
    }
$ javac *.java
$ java Main
Idade lida: 29
Problema: linha 2: idade inválida
Causa: java.lang.NumberFormatException: For input string: "trinta"
Idade lida: 22
Sempre passe a causa

throw new ImportacaoException("linha 2: idade inválida");, sem o e, também compila. Mas o Caused by: desaparece, e com ele a informação de que o problema foi o texto "trinta". Quem for investigar vai ter só metade da história. Ao transformar uma exceção em outra, passe a original como causa. As exceções da biblioteca também aceitam: new RuntimeException("mensagem", e), new IllegalStateException("mensagem", e).


Quando vale a pena criar uma exceção

SituaçãoCrie uma?
Valor de parâmetro inválido (idade negativa, nota 11)Não: IllegalArgumentException já diz isso
Método chamado na hora erradaNão: IllegalStateException
Uma regra do negócio que quem chama vai querer tratar separadamente (saldo insuficiente, estoque esgotado, aluno não matriculado)Sim
Você precisa levar dados junto (quanto faltou, qual linha)Sim
Você quer esconder detalhes técnicos atrás de um nome do seu domínio (a importação falhou, seja por arquivo ou por número inválido)Sim, com a causa
Só para ter um nome bonito, sem ninguém capturar por eleProvavelmente não

Boas práticas, em resumo: nome terminando em Exception; pelo menos os construtores (String mensagem) e (String mensagem, Throwable causa); mensagens com os valores envolvidos; checked ou unchecked escolhido de propósito.

Um aviso que você pode encontrar: serialVersionUID

Compilando com todos os avisos ligados (-Xlint), o javac comenta sobre as suas exceções:

$ javac -Xlint *.java
SaldoInsuficienteException.java:1: warning: [serial] serializable class SaldoInsuficienteException has no definition of serialVersionUID
public class SaldoInsuficienteException extends Exception {
       ^
1 warning

Exceções são "serializáveis" (podem ser transformadas em bytes para viajar pela rede), e o javac sugere um número de versão para isso. É só um aviso: o programa compila e roda normalmente, e sem -Xlint ele nem aparece. Se quiser silenciá-lo, acrescente na classe private static final long serialVersionUID = 1L;.


Quando dá errado

"cannot be converted to Throwable" (esqueceu o extends)
public class SaldoInsuficienteException {
    public SaldoInsuficienteException(String mensagem) {
    }
}
$ javac *.java
Conta.java:10: error: incompatible types: SaldoInsuficienteException cannot be converted to Throwable
    public void sacar(double valor) throws SaldoInsuficienteException {
                                           ^
Conta.java:12: error: incompatible types: SaldoInsuficienteException cannot be converted to Throwable
            throw new SaldoInsuficienteException(
            ^
Main.java:8: error: incompatible types: SaldoInsuficienteException cannot be converted to Throwable
        } catch (SaldoInsuficienteException e) {
                 ^
Main.java:9: error: cannot find symbol
            System.out.println("Saque negado: " + e.getMessage());
                                                   ^
  symbol:   method getMessage()
  location: variable e of type SaldoInsuficienteException
4 errors

O que aconteceu: sem extends, a classe não é uma exceção, só uma classe comum que tem "Exception" no nome. Ela não pode aparecer em throws, throw nem catch, e não tem getMessage(). Quatro erros, uma causa só.

Como resolver: extends Exception (ou RuntimeException).

"constructor … cannot be applied to given types" (falta o construtor)
public class SaldoInsuficienteException extends Exception {
}
$ javac *.java
Conta.java:12: error: constructor SaldoInsuficienteException in class SaldoInsuficienteException cannot be applied to given types;
            throw new SaldoInsuficienteException(
                  ^
  required: no arguments
  found:    String
  reason: actual and formal argument lists differ in length
1 error

O que aconteceu: construtores não são herdados. A classe só tem o construtor padrão, sem parâmetros (required: no arguments, "exigido: nenhum argumento"), e a Conta tentou passar um texto (found: String, "encontrado: String").

Como resolver: escreva o construtor public SaldoInsuficienteException(String mensagem) { super(mensagem); }.

A mensagem aparece como null
public class SaldoInsuficienteException extends Exception {
    public SaldoInsuficienteException(String mensagem) {
    }
}
$ javac *.java
$ java Main
Ana sacou 30.0. Saldo: 70.0
Saque negado: null

O que aconteceu: o construtor recebeu a mensagem e não fez nada com ela. Sem super(mensagem), o Java chama sozinho o super() sem parâmetros, e a Exception fica sem mensagem: getMessage() devolve null.

Como resolver: a primeira linha do construtor deve ser super(mensagem);.


Referência

SintaxeO que faz
class XException extends ExceptionExceção própria checked
class XException extends RuntimeExceptionExceção própria unchecked
public XException(String m) { super(m); }Construtor com mensagem
public XException(String m, Throwable c) { super(m, c); }Construtor com mensagem e causa
throw new XException("…", e);Lança guardando a exceção original como causa
e.getCause()Devolve a causa (ou null)
Caused by: no stack traceA causa, com o rastro dela
... N moreN molduras iguais às da exceção de cima, omitidas
private static final long serialVersionUID = 1L;Silencia o aviso [serial] do -Xlint

Exercícios

1. A mãe certa

Complete para criar uma exceção unchecked:

public class EstoqueEsgotadoException extends { … }

2. Repassar a mensagem

Complete o construtor:

public EstoqueEsgotadoException(String mensagem) { }

3. Guardar a causa

Dentro de catch (NumberFormatException e), complete para lançar a sua exceção levando a original junto:

throw new ImportacaoException("idade inválida", );

4. Você criou class AlunoNaoEncontradoException extends Exception. O que acontece com quem chama um método que a lança?

5. O que a linha Caused by: de um stack trace mostra?

6. Um método recebe um CPF com 9 dígitos. O que é mais adequado?

Missão final: crie, na aba NotaInvalidaException.java, uma exceção unchecked (herdando de RuntimeException) com construtor que recebe a mensagem. Depois, no Main, troque a IllegalArgumentException por ela. O programa deve registrar 9.5 e terminar com a NotaInvalidaException.

public class Main {
    static void registrarNota(double nota) {
        if (nota < 0 || nota > 10) {
            throw new IllegalArgumentException("nota fora de 0 a 10: " + nota);
        }
        System.out.println("Nota registrada: " + nota);
    }

    public static void main(String[] args) {
        registrarNota(9.5);
        registrarNota(11);
    }
}
public class NotaInvalidaException {
}
public class Main {
    static void registrarNota(double nota) {
        if (nota < 0 || nota > 10) {
            throw new NotaInvalidaException("nota fora de 0 a 10: " + nota);
        }
        System.out.println("Nota registrada: " + nota);
    }

    public static void main(String[] args) {
        registrarNota(9.5);
        registrarNota(11);
    }
}
public class NotaInvalidaException extends RuntimeException {
    public NotaInvalidaException(String mensagem) {
        super(mensagem);
    }
}

Resumo

  • Uma exceção própria é uma classe que herda de Exception (checked) ou RuntimeException (unchecked), com nome terminando em Exception.
  • O construtor repassa a mensagem com super(mensagem); sem isso, getMessage() devolve null.
  • Ela pode ter atributos e métodos (getFaltando()) para levar dados a quem captura.
  • Checked obriga quem chama a tratar ou declarar; unchecked deixa o código mais limpo. Escolha de propósito.
  • Ao trocar uma exceção por outra, passe a original como causa (super(mensagem, causa)): ela aparece em getCause() e no Caused by:.
  • Crie exceções quando quem chama vai tratar aquele caso separadamente ou precisa de dados extras; para parâmetro inválido ou estado errado, use as prontas.