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.
- 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 oCaused 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 emException. - 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. É aExceptionquem guarda o texto e o devolve emgetMessage()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.
finalporque 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étodo | Obrigatório | Opcional (vale como documentação) |
| Quem chama | Precisa tratar ou declarar, senão não compila | Trata se quiser |
| Vantagem | Ninguém esquece de pensar no caso | Código mais limpo; os métodos do meio não mudam |
| Combina com | Situações esperadas que quem chama deve resolver | Regras 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 é
Throwablepara aceitar qualquer uma. - super(mensagem, causa);
- A
Exceptiontem um construtor que recebe as duas coisas e guarda a causa. Depois, ela aparece emgetCause()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
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ção | Crie uma? |
|---|---|
| Valor de parâmetro inválido (idade negativa, nota 11) | Não: IllegalArgumentException já diz isso |
| Método chamado na hora errada | Nã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 ele | Provavelmente 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.
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
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).
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); }.
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
| Sintaxe | O que faz |
|---|---|
class XException extends Exception | Exceção própria checked |
class XException extends RuntimeException | Exceçã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 trace | A causa, com o rastro dela |
... N more | N 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:
2. Repassar a mensagem
Complete o construtor:
3. Guardar a causa
Dentro de catch (NumberFormatException e), complete para lançar a sua exceção levando a original junto:
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) ouRuntimeException(unchecked), com nome terminando emException. - O construtor repassa a mensagem com
super(mensagem); sem isso,getMessage()devolvenull. - 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 emgetCause()e noCaused 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.